Merge commit 'a2cce1cdf11c888a85941ce82f8446bc53d093cb' into dev-release
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 9bfdfcd..036dd57 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -289,10 +289,10 @@
     /**
      * Add main-dex classes.
      *
-     * Add classes to keep in the primary dex file (<code>classes.dex</code>).
+     * <p>Add classes to keep in the primary dex file (<code>classes.dex</code>).
      *
-     * NOTE: The name of the classes is specified using the Java fully qualified names format
-     * (e.g. "com.example.MyClass"), and <i>not</i> the format used by the main-dex list file.
+     * <p>NOTE: The name of the classes is specified using the Java fully qualified names format
+     * (e.g. "com.example.MyClass$A"), and <i>not</i> the format used by the main-dex list file.
      */
     public B addMainDexClasses(String... classes) {
       guard(() -> app.addMainDexClasses(classes));
diff --git a/src/main/java/com/android/tools/r8/CompatDxHelper.java b/src/main/java/com/android/tools/r8/CompatDxHelper.java
index beec3dc..13cd385 100644
--- a/src/main/java/com/android/tools/r8/CompatDxHelper.java
+++ b/src/main/java/com/android/tools/r8/CompatDxHelper.java
@@ -25,8 +25,4 @@
   public static void ignoreDexInArchive(BaseCommand.Builder builder) {
     builder.setIgnoreDexInArchive(true);
   }
-
-  public static void enableDesugarBackportStatics(D8Command.Builder builder) {
-    builder.enableDesugarBackportStatics();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index ab0d4dd..0a38fb5 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.origin.CommandLineOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.synthesis.SyntheticFinalization;
+import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.ExceptionUtils;
@@ -180,6 +181,7 @@
       options.disableGlobalOptimizations();
 
       AppView<AppInfo> appView = readApp(inputApp, options, executor, timing);
+      SyntheticItems.collectSyntheticInputs(appView);
 
       final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index cb00bb7..9e462dd 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -163,12 +163,6 @@
       return self();
     }
 
-    // Internal helper for compat tools to make them only desugar backports.
-    Builder enableDesugarBackportStatics() {
-      this.desugarState = DesugarState.ONLY_BACKPORT_STATICS;
-      return self();
-    }
-
     @Override
     Builder self() {
       return this;
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
index f6a10a9..07569a7 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -223,7 +223,7 @@
   @Override
   InternalOptions getInternalOptions() {
     InternalOptions internal = new InternalOptions(factory, reporter);
-    internal.programConsumer = ClassFileConsumer.emptyConsumer();
+    internal.programConsumer = DexIndexedConsumer.emptyConsumer();
     internal.mainDexKeepRules = mainDexKeepRules;
     internal.mainDexListConsumer = mainDexListConsumer;
     internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index c98bafd..16d3642 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -978,7 +978,7 @@
   }
 
   private static boolean verifyOriginalMethodInPosition(CfCode code, DexMethod originalMethod) {
-    for (CfInstruction instruction : code.instructions) {
+    for (CfInstruction instruction : code.getInstructions()) {
       if (!instruction.isPosition()) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 4256db3..5dfc5cb 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -120,7 +120,7 @@
         registry.registerInvokeInterface(method);
         break;
       case STATIC:
-        registry.registerInvokeStatic(method);
+        registry.registerInvokeStatic(method, itf);
         break;
       case SUPER:
         registry.registerInvokeSuper(method);
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 bb5706d..5b45b7b 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -42,6 +42,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -100,6 +101,7 @@
   private final MethodToCodeObjectMapping codeMapping;
   private final DexApplication application;
   private final InternalOptions options;
+  private final GraphLens graphLens;
   private final NamingLens namingLens;
   private final DexOutputBuffer dest;
   private final MixedSectionOffsets mixedSectionOffsets;
@@ -118,6 +120,7 @@
     this.codeMapping = codeMapping;
     this.application = application;
     this.options = options;
+    this.graphLens = mapping.getGraphLens();
     this.namingLens = namingLens;
     this.dest = new DexOutputBuffer(provider);
     this.mixedSectionOffsets = new MixedSectionOffsets(options, codeMapping);
@@ -416,8 +419,7 @@
         result += LebUtils
             .sizeAsSleb128(hasCatchAll ? -handler.pairs.length : handler.pairs.length);
         for (TypeAddrPair pair : handler.pairs) {
-
-          result += sizeAsUleb128(mapping.getOffsetFor(pair.type));
+          result += sizeAsUleb128(mapping.getOffsetFor(pair.getType(graphLens)));
           result += sizeAsUleb128(pair.addr);
         }
         if (hasCatchAll) {
@@ -525,9 +527,9 @@
         boolean hasCatchAll = handler.catchAllAddr != TryHandler.NO_HANDLER;
         dest.putSleb128(hasCatchAll ? -handler.pairs.length : handler.pairs.length);
         for (TypeAddrPair pair : handler.pairs) {
-          dest.putUleb128(mapping.getOffsetFor(pair.type));
+          dest.putUleb128(mapping.getOffsetFor(pair.getType(graphLens)));
           dest.putUleb128(pair.addr);
-          desugaredLibraryCodeToKeep.recordClass(pair.type);
+          desugaredLibraryCodeToKeep.recordClass(pair.getType(graphLens));
         }
         if (hasCatchAll) {
           dest.putUleb128(handler.catchAllAddr);
diff --git a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
index 128ea1d..d1f8749 100644
--- a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
+++ b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
@@ -206,7 +206,7 @@
       TypeAddrPair[] newPairs = new TypeAddrPair[handler.pairs.length];
       for (int j = 0; j < handler.pairs.length; j++) {
         TypeAddrPair pair = handler.pairs[j];
-        newPairs[j] = new TypeAddrPair(pair.type, it.next().getOffset());
+        newPairs[j] = new TypeAddrPair(pair.getType(), it.next().getOffset());
       }
       result[i] = new TryHandler(newPairs, catchAllAddr);
     }
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 d2d137b..114bc8a 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -32,8 +32,7 @@
 
   public static AppInfo createInitialAppInfo(
       DexApplication application, MainDexClasses mainDexClasses) {
-    return new AppInfo(
-        SyntheticItems.createInitialSyntheticItems().commit(application), mainDexClasses);
+    return new AppInfo(SyntheticItems.createInitialSyntheticItems(application), mainDexClasses);
   }
 
   public AppInfo(CommittedItems committedItems, MainDexClasses mainDexClasses) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index a1e3d15..ace1d56 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -54,7 +54,7 @@
       ClassToFeatureSplitMap classToFeatureSplitMap,
       MainDexClasses mainDexClasses) {
     return new AppInfoWithClassHierarchy(
-        SyntheticItems.createInitialSyntheticItems().commit(application),
+        SyntheticItems.createInitialSyntheticItems(application),
         classToFeatureSplitMap,
         mainDexClasses);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index f4407b4..3850a41 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -78,6 +78,7 @@
   private HorizontallyMergedClasses horizontallyMergedClasses;
   private VerticallyMergedClasses verticallyMergedClasses;
   private EnumValueInfoMapCollection unboxedEnums = EnumValueInfoMapCollection.empty();
+  // TODO(b/169115389): Remove
   private Set<DexMethod> cfByteCodePassThrough = ImmutableSet.of();
 
   private Map<DexClass, DexValueString> sourceDebugExtensions = new IdentityHashMap<>();
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 b6355dc..02af4ee 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -126,7 +126,7 @@
 
   private final int maxStack;
   private int maxLocals;
-  public List<CfInstruction> instructions;
+  private List<CfInstruction> instructions;
   private final List<CfTryCatch> tryCatchRanges;
   private final List<LocalVariableInfo> localVariables;
 
@@ -169,6 +169,10 @@
     return Collections.unmodifiableList(instructions);
   }
 
+  public void setInstructions(List<CfInstruction> instructions) {
+    this.instructions = instructions;
+  }
+
   public List<LocalVariableInfo> getLocalVariables() {
     return Collections.unmodifiableList(localVariables);
   }
@@ -242,6 +246,13 @@
   }
 
   private boolean shouldAddParameterNames(DexEncodedMethod method, AppView<?> appView) {
+    // In cf to cf desugar we do pass through of code and don't move around methods.
+    // TODO(b/169115389): Remove when we have a way to determine if we need parameter names per
+    // method.
+    if (appView.options().cfToCfDesugar) {
+      return false;
+    }
+
     // Don't add parameter information if the code already has full debug information.
     // Note: This fast path can cause a method to loose its parameter info, if the debug info turned
     // out to be invalid during IR building.
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 ade8d06..5bdddd9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -77,6 +77,7 @@
     }
     if (annotation == options.itemFactory.dalvikFastNativeAnnotation
         || annotation == options.itemFactory.dalvikCriticalNativeAnnotation
+        || annotation == options.itemFactory.annotationSynthesizedClass
         || annotation == options.itemFactory.annotationSynthesizedClassMap) {
       return true;
     }
@@ -347,7 +348,7 @@
         throw new CompilationError(getInvalidSynthesizedClassMapMessage(clazz, foundAnnotation));
       }
       DexAnnotationElement value = foundAnnotation.annotation.elements[0];
-      if (!value.name.toSourceString().equals("value")) {
+      if (value.name != dexItemFactory.valueString) {
         throw new CompilationError(getInvalidSynthesizedClassMapMessage(clazz, foundAnnotation));
       }
       DexValueArray existingList = value.value.asDexValueArray();
@@ -375,6 +376,43 @@
         + ": " + invalidAnnotation.toString();
   }
 
+  public static DexAnnotation createAnnotationSynthesizedClass(
+      DexType synthesizingContext, DexItemFactory dexItemFactory) {
+    DexValueType value = new DexValueType(synthesizingContext);
+    DexAnnotationElement element = new DexAnnotationElement(dexItemFactory.valueString, value);
+    return new DexAnnotation(
+        VISIBILITY_BUILD,
+        new DexEncodedAnnotation(
+            dexItemFactory.annotationSynthesizedClass, new DexAnnotationElement[] {element}));
+  }
+
+  public static boolean hasSynthesizedClassAnnotation(
+      DexAnnotationSet annotations, DexItemFactory factory) {
+    return getSynthesizedClassAnnotationContextType(annotations, factory) != null;
+  }
+
+  public static DexType getSynthesizedClassAnnotationContextType(
+      DexAnnotationSet annotations, DexItemFactory factory) {
+    if (annotations.size() != 1) {
+      return null;
+    }
+    DexAnnotation annotation = annotations.annotations[0];
+    if (annotation.annotation.type != factory.annotationSynthesizedClass) {
+      return null;
+    }
+    if (annotation.annotation.elements.length != 1) {
+      return null;
+    }
+    DexAnnotationElement element = annotation.annotation.elements[0];
+    if (element.name != factory.valueString) {
+      return null;
+    }
+    if (!element.value.isDexValueType()) {
+      return null;
+    }
+    return element.value.asDexValueType().getValue();
+  }
+
   public static DexAnnotation createAnnotationSynthesizedClassMap(
       TreeSet<DexType> synthesized,
       DexItemFactory dexItemFactory) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index ce47f1e..eaf8d7f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -573,7 +573,7 @@
 
     public static class TypeAddrPair extends DexItem implements Comparable<TypeAddrPair> {
 
-      public final DexType type;
+      private final DexType type;
       public final /* offset */ int addr;
 
       public TypeAddrPair(DexType type, int addr) {
@@ -581,8 +581,16 @@
         this.addr = addr;
       }
 
+      public DexType getType() {
+        return type;
+      }
+
+      public DexType getType(GraphLens lens) {
+        return lens.lookupType(type);
+      }
+
       public void collectIndexedItems(IndexedItemCollection indexedItems, GraphLens graphLens) {
-        DexType rewritten = graphLens.lookupType(type);
+        DexType rewritten = getType(graphLens);
         rewritten.collectIndexedItems(indexedItems);
       }
 
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 4936ceb..f049ec5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -281,8 +281,8 @@
     // TODO(b/158159959): Implement a more precise hashing on code objects.
     if (code.isCfCode()) {
       CfCode cfCode = code.asCfCode();
-      hasher.putInt(cfCode.instructions.size());
-      for (CfInstruction instruction : cfCode.instructions) {
+      hasher.putInt(cfCode.getInstructions().size());
+      for (CfInstruction instruction : cfCode.getInstructions()) {
         hasher.putInt(instruction.getClass().hashCode());
       }
     } else {
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 4c3779e..18ebdb4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -297,6 +297,8 @@
 
   public final DexString throwableArrayDescriptor = createString("[Ljava/lang/Throwable;");
 
+  public final DexString valueString = createString("value");
+
   public final DexType booleanType = createStaticallyKnownType(booleanDescriptor);
   public final DexType byteType = createStaticallyKnownType(byteDescriptor);
   public final DexType charType = createStaticallyKnownType(charDescriptor);
@@ -543,6 +545,8 @@
   public final DexType annotationSourceDebugExtension =
       createStaticallyKnownType("Ldalvik/annotation/SourceDebugExtension;");
   public final DexType annotationThrows = createStaticallyKnownType("Ldalvik/annotation/Throws;");
+  public final DexType annotationSynthesizedClass =
+      createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClass;");
   public final DexType annotationSynthesizedClassMap =
       createStaticallyKnownType("Lcom/android/tools/r8/annotations/SynthesizedClassMap;");
   public final DexType annotationCovariantReturnType =
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 7bfa6cb..b276437 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -986,12 +986,16 @@
         // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win
         //  that only subclasses which are known to need it actually do it?
         DexMethod rewrittenReboundReference = previous.getRewrittenReboundReference(methodMap);
-        return MethodLookupResult.builder(this)
-            .setReboundReference(rewrittenReboundReference)
-            .setReference(
+        DexMethod rewrittenReference =
+            previous.getReference() == previous.getReboundReference()
+                ? rewrittenReboundReference
+                : // This assumes that the holder will always be moved in lock-step with the method!
                 rewrittenReboundReference.withHolder(
                     internalDescribeLookupClassType(previous.getReference().getHolderType()),
-                    dexItemFactory))
+                    dexItemFactory);
+        return MethodLookupResult.builder(this)
+            .setReboundReference(rewrittenReboundReference)
+            .setReference(rewrittenReference)
             .setPrototypeChanges(
                 internalDescribePrototypeChanges(
                     previous.getPrototypeChanges(), rewrittenReboundReference))
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index 263a21b..f783b63 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -248,7 +248,9 @@
       Origin origin,
       boolean shouldApplyCodeRewritings) {
     if (!cfCode.verifyFrames(method, appView, origin, shouldApplyCodeRewritings)) {
-      cfCode.instructions.removeIf(CfInstruction::isFrame);
+      ArrayList<CfInstruction> newInstructions = new ArrayList<>(cfCode.getInstructions());
+      newInstructions.removeIf(CfInstruction::isFrame);
+      cfCode.setInstructions(newInstructions);
     }
     return cfCode;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index 5830357..033182e 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -45,6 +45,10 @@
     registerInstanceFieldWrite(field);
   }
 
+  public void registerInvokeStatic(DexMethod method, boolean itf) {
+    registerInvokeStatic(method);
+  }
+
   public abstract void registerNewInstance(DexType type);
 
   public abstract void registerStaticFieldRead(DexField field);
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
index c57a3da..d60a589 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ClassInitializerAssertionEnablingAnalysis.java
@@ -102,8 +102,8 @@
       ImmutableList.of(CfConstNumber.class, CfLogicalBinop.class, CfFieldInstruction.class);
 
   private boolean hasJavacClinitAssertionCode(CfCode code) {
-    for (int i = 0; i < code.instructions.size(); i++) {
-      CfInstruction instruction = code.instructions.get(i);
+    for (int i = 0; i < code.getInstructions().size(); i++) {
+      CfInstruction instruction = code.getInstructions().get(i);
       if (instruction.isInvoke()) {
         // Check for the generated instruction sequence by looking for the call to
         // desiredAssertionStatus() followed by the expected instruction types and finally checking
@@ -132,8 +132,9 @@
   private boolean hasKotlincClinitAssertionCode(ProgramMethod method) {
     if (method.getHolderType() == dexItemFactory.kotlin.assertions.type) {
       CfCode code = method.getDefinition().getCode().asCfCode();
-      for (int i = 1; i < code.instructions.size(); i++) {
-        CfInstruction instruction = code.instructions.get(i - 1);
+      List<CfInstruction> instructions = code.getInstructions();
+      for (int i = 1; i < instructions.size(); i++) {
+        CfInstruction instruction = instructions.get(i - 1);
         if (instruction.isInvoke()) {
           // Check for the generated instruction sequence by looking for the call to
           // desiredAssertionStatus() followed by the expected instruction types and finally
@@ -141,8 +142,8 @@
           CfInvoke invoke = instruction.asInvoke();
           if (invoke.getOpcode() == Opcodes.INVOKEVIRTUAL
               && invoke.getMethod() == dexItemFactory.classMethods.desiredAssertionStatus) {
-            if (code.instructions.get(i).isFieldInstruction()) {
-              CfFieldInstruction fieldInstruction = code.instructions.get(i).asFieldInstruction();
+            if (instructions.get(i).isFieldInstruction()) {
+              CfFieldInstruction fieldInstruction = instructions.get(i).asFieldInstruction();
               if (fieldInstruction.getOpcode() == Opcodes.PUTSTATIC
                   && fieldInstruction.getField().name == kotlinAssertionsEnabled) {
                 return true;
@@ -164,9 +165,9 @@
     int nextExpectedInstructionIndex = 0;
     CfInstruction instruction = null;
     for (int i = fromIndex;
-        i < code.instructions.size() && nextExpectedInstructionIndex < sequence.size();
+        i < code.getInstructions().size() && nextExpectedInstructionIndex < sequence.size();
         i++) {
-      instruction = code.instructions.get(i);
+      instruction = code.getInstructions().get(i);
       if (instruction.isLabel() || instruction.isFrame()) {
         // Just ignore labels and frames.
         continue;
@@ -186,9 +187,9 @@
     int nextExpectedInstructionIndex = 0;
     CfInstruction instruction = null;
     for (int i = fromIndex;
-        i < code.instructions.size() && nextExpectedInstructionIndex < sequence.size();
+        i < code.getInstructions().size() && nextExpectedInstructionIndex < sequence.size();
         i++) {
-      instruction = code.instructions.get(i);
+      instruction = code.getInstructions().get(i);
       if (instruction.isStore() || instruction.isLoad()) {
         // Just ignore stores and loads.
         continue;
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/CanOnlyMergeIntoClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/CanOnlyMergeIntoClassPolicy.java
new file mode 100644
index 0000000..fa9873a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/CanOnlyMergeIntoClassPolicy.java
@@ -0,0 +1,17 @@
+// 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+public abstract class CanOnlyMergeIntoClassPolicy extends SingleClassPolicy {
+  public abstract boolean canOnlyMergeInto(DexProgramClass clazz);
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    // TODO(b/165577835): Allow merging of classes that must be the target of their merge group.
+    return !canOnlyMergeInto(program);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index c993870..d33b15d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -17,8 +17,10 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NoStaticClassInitializer;
 import com.android.tools.r8.horizontalclassmerging.policies.NotEntryPoint;
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
+import com.android.tools.r8.horizontalclassmerging.policies.PreventChangingVisibility;
 import com.android.tools.r8.horizontalclassmerging.policies.PreventMergeIntoMainDex;
 import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
+import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
 import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ClassMergingEnqueuerExtension;
@@ -56,6 +58,8 @@
             new NotEntryPoint(appView.dexItemFactory()),
             new PreventMergeIntoMainDex(appView, mainDexTracingResult),
             new SameParentClass(),
+            new PreventChangingVisibility(),
+            new SameFeatureSplit(appView),
             new RespectPackageBoundaries(appView),
             new DontMergeSynchronizedClasses(appView)
             // TODO: add policies
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
new file mode 100644
index 0000000..3407306
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
@@ -0,0 +1,26 @@
+// 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+public abstract class MultiClassSameReferencePolicy<T> extends MultiClassPolicy {
+
+  @Override
+  public final Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+    Map<T, Collection<DexProgramClass>> groups = new IdentityHashMap<>();
+    for (DexProgramClass clazz : group) {
+      groups.computeIfAbsent(getMergeKey(clazz), ignore -> new LinkedList<>()).add(clazz);
+    }
+    removeTrivialGroups(groups.values());
+    return groups.values();
+  }
+
+  public abstract T getMergeKey(DexProgramClass clazz);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
index d8dea98..8d6fc72 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -8,4 +8,8 @@
  * The super class of all horizontal class merging policies. Most classes will either implement
  * {@link SingleClassPolicy} or {@link MultiClassPolicy}.
  */
-public abstract class Policy {}
+public abstract class Policy {
+  public boolean shouldSkipPolicy() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
index 322958f..f0ac605 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -54,6 +54,10 @@
     }
 
     for (Policy policy : policies) {
+      if (policy.shouldSkipPolicy()) {
+        continue;
+      }
+
       if (policy instanceof SingleClassPolicy) {
         linkedGroups = applySingleClassPolicy((SingleClassPolicy) policy, linkedGroups);
       } else if (policy instanceof MultiClassPolicy) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java
new file mode 100644
index 0000000..18be993
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java
@@ -0,0 +1,23 @@
+// 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class NoServiceLoaders extends SingleClassPolicy {
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoServiceLoaders(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    return !appView.appServices().allServiceTypes().contains(program.getType());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java
new file mode 100644
index 0000000..3c317eb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java
@@ -0,0 +1,78 @@
+// 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class PreventChangingVisibility extends MultiClassPolicy {
+  public PreventChangingVisibility() {}
+
+  public static class TargetGroup {
+    private final Collection<DexProgramClass> group = new LinkedList<>();
+    private final Map<Wrapper<DexMethod>, MethodAccessFlags> methodMap = new HashMap<>();
+
+    public Collection<DexProgramClass> getGroup() {
+      return group;
+    }
+
+    public boolean tryAdd(DexProgramClass clazz) {
+      Map<Wrapper<DexMethod>, MethodAccessFlags> newMethods = new HashMap<>();
+      for (DexEncodedMethod method : clazz.methods()) {
+        Wrapper<DexMethod> methodSignature =
+            MethodSignatureEquivalence.get().wrap(method.getReference());
+        MethodAccessFlags flags = methodMap.get(methodSignature);
+
+        if (flags == null) {
+          newMethods.put(methodSignature, method.getAccessFlags());
+        } else {
+          if (!flags.isSameVisibility(method.getAccessFlags())) {
+            return false;
+          }
+        }
+      }
+
+      methodMap.putAll(newMethods);
+      group.add(clazz);
+      return true;
+    }
+  }
+
+  @Override
+  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+    List<TargetGroup> groups = new ArrayList<>();
+
+    for (DexProgramClass clazz : group) {
+      boolean added = Iterables.any(groups, targetGroup -> targetGroup.tryAdd(clazz));
+      if (!added) {
+        TargetGroup newGroup = new TargetGroup();
+        added = newGroup.tryAdd(clazz);
+        assert added;
+        groups.add(newGroup);
+      }
+    }
+
+    Collection<Collection<DexProgramClass>> newGroups = new ArrayList<>();
+    for (TargetGroup newGroup : groups) {
+      if (!isTrivial(newGroup.getGroup())) {
+        newGroups.add(newGroup.getGroup());
+      }
+    }
+
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java
new file mode 100644
index 0000000..9e4dd2b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFeatureSplit.java
@@ -0,0 +1,24 @@
+// 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class SameFeatureSplit extends MultiClassSameReferencePolicy<FeatureSplit> {
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public SameFeatureSplit(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public FeatureSplit getMergeKey(DexProgramClass clazz) {
+    return appView.appInfo().getClassToFeatureSplitMap().getFeatureSplit(clazz);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
index 8c8eeb9..5926ecc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/TypeChecker.java
@@ -26,14 +26,17 @@
  * that it is dead.
  *
  * <p>Pruning code that does not verify is necessary in order to be able to assert that the types
- * are sound using {@link Instruction#verifyTypes(AppView)}.
+ * are sound using {@link Instruction#verifyTypes(AppView, VerifyTypesHelper)}.
  */
 public class TypeChecker {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final VerifyTypesHelper verifyTypesHelper;
 
-  public TypeChecker(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public TypeChecker(
+      AppView<? extends AppInfoWithClassHierarchy> appView, VerifyTypesHelper verifyTypesHelper) {
     this.appView = appView;
+    this.verifyTypesHelper = verifyTypesHelper;
   }
 
   public boolean check(IRCode code) {
@@ -70,7 +73,7 @@
     TypeElement valueType = instruction.returnValue().getType();
     TypeElement returnType =
         TypeElement.fromDexType(method.method.proto.returnType, Nullability.maybeNull(), appView);
-    if (isSubtypeOf(valueType, returnType)) {
+    if (verifyTypesHelper.isAssignable(valueType, returnType)) {
       return true;
     }
 
@@ -93,7 +96,7 @@
     TypeElement valueType = instruction.value().getType();
     TypeElement fieldType =
         TypeElement.fromDexType(instruction.getField().type, valueType.nullability(), appView);
-    if (isSubtypeOf(valueType, fieldType)) {
+    if (verifyTypesHelper.isAssignable(valueType, fieldType)) {
       return true;
     }
 
@@ -112,12 +115,6 @@
     TypeElement throwableType =
         TypeElement.fromDexType(
             appView.dexItemFactory().throwableType, valueType.nullability(), appView);
-    return isSubtypeOf(valueType, throwableType);
-  }
-
-  private boolean isSubtypeOf(TypeElement expectedSubtype, TypeElement expectedSupertype) {
-    return (expectedSubtype.isNullType() && expectedSupertype.isReferenceType())
-        || expectedSubtype.lessThanOrEqual(expectedSupertype, appView)
-        || expectedSubtype.isBasedOnMissingClass(appView);
+    return verifyTypesHelper.isAssignable(valueType, throwableType);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/VerifyTypesHelper.java b/src/main/java/com/android/tools/r8/ir/analysis/VerifyTypesHelper.java
new file mode 100644
index 0000000..d8c1bb1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/VerifyTypesHelper.java
@@ -0,0 +1,54 @@
+// 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.ir.analysis;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+
+public class VerifyTypesHelper {
+
+  private final AppView<?> appView;
+
+  private VerifyTypesHelper(AppView<?> appView) {
+    this.appView = appView;
+  }
+
+  public static VerifyTypesHelper create(AppView<?> appView) {
+    return new VerifyTypesHelper(appView);
+  }
+
+  public boolean isAssignable(TypeElement one, TypeElement other) {
+    if (one.isPrimitiveType() != other.isPrimitiveType()) {
+      return false;
+    }
+    if (one.isPrimitiveType()) {
+      assert other.isPrimitiveType();
+      return one.equals(other);
+    }
+    assert one.isReferenceType() && other.isReferenceType();
+    if (one.isNullType() && other.isReferenceType()) {
+      return true;
+    }
+    if (one.isArrayType() != other.isArrayType()) {
+      return one.isArrayType()
+          && other.asClassType().getClassType() == appView.dexItemFactory().objectType;
+    }
+    if (one.isArrayType()) {
+      assert other.isArrayType();
+      return isAssignable(one.asArrayType().getMemberType(), other.asArrayType().getMemberType());
+    }
+    assert one.isClassType() && other.isClassType();
+    if (appView.enableWholeProgramOptimizations()) {
+      return one.lessThanOrEqual(other, appView)
+          || one.isBasedOnMissingClass(appView.withClassHierarchy());
+    } else {
+      // If we do not have whole program knowledge, we can only do the most basic check.
+      if (one.asClassType().getClassType() == appView.dexItemFactory().objectType) {
+        return other.asClassType().getClassType() == appView.dexItemFactory().objectType;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Assume.java b/src/main/java/com/android/tools/r8/ir/code/Assume.java
index 7aed1d9..4674e14 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Assume.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Assume.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
@@ -239,8 +240,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    assert super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, verifyTypesHelper);
 
     TypeElement inType = src().getType();
     assert inType.isReferenceType() : inType;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index fa3c34c..c4823bc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 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.Phi.RegisterReadType;
@@ -81,8 +82,9 @@
     return true;
   }
 
-  public boolean verifyTypes(AppView<?> appView) {
-    assert instructions.stream().allMatch(instruction -> instruction.verifyTypes(appView));
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert instructions.stream()
+        .allMatch(instruction -> instruction.verifyTypes(appView, verifyTypesHelper));
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 7085e80..6722cfb 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
@@ -165,8 +166,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    assert super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, verifyTypesHelper);
 
     TypeElement inType = object().getType();
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index dc66e3c..763d520 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.constant.Bottom;
 import com.android.tools.r8.ir.analysis.constant.ConstLatticeElement;
 import com.android.tools.r8.ir.analysis.constant.LatticeElement;
@@ -323,8 +324,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    assert super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, verifyTypesHelper);
     assert !isZero() || getOutType().isPrimitiveType() || getOutType().isNullType();
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index 0a76de9..93b95e7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
@@ -177,8 +178,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    assert super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, verifyTypesHelper);
     TypeElement expectedType = TypeElement.stringClassType(appView, definitelyNotNull());
     assert getOutType().equals(expectedType);
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
index f8a36db..6817315 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 
 /**
@@ -77,9 +78,9 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    super.verifyTypes(appView);
-    assert src().getType().lessThanOrEqual(getOutType(), appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    super.verifyTypes(appView, verifyTypesHelper);
+    assert verifyTypesHelper.isAssignable(src().getType(), getOutType());
     return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 476135f..90e6097 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.analysis.TypeChecker;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -616,11 +617,12 @@
   public boolean verifyTypes(AppView<?> appView) {
     // We can only type check the program if we have subtyping information. Therefore, we do not
     // require that the program type checks in D8.
+    VerifyTypesHelper verifyTypesHelper = VerifyTypesHelper.create(appView);
     if (appView.enableWholeProgramOptimizations()) {
       assert validAssumeInstructions(appView);
-      assert new TypeChecker(appView.withLiveness()).check(this);
+      assert new TypeChecker(appView.withLiveness(), verifyTypesHelper).check(this);
     }
-    assert blocks.stream().allMatch(block -> block.verifyTypes(appView));
+    assert blocks.stream().allMatch(block -> block.verifyTypes(appView, verifyTypesHelper));
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 1334a46..6a42c80 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.constant.Bottom;
 import com.android.tools.r8.ir.analysis.constant.ConstRangeLatticeElement;
 import com.android.tools.r8.ir.analysis.constant.LatticeElement;
@@ -1393,8 +1394,7 @@
         "Implement type lattice evaluation for: " + getInstructionName());
   }
 
-  public boolean verifyTypes(AppView<?> appView) {
-    // TODO(b/72693244): for instructions with invariant out type, we can verify type directly here.
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
     if (outValue != null) {
       TypeElement outTypeElement = outValue.getType();
       if (outTypeElement.isArrayType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 79f7152..b006d6a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -115,8 +116,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    assert super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    assert super.verifyTypes(appView, verifyTypesHelper);
 
     Value receiver = getReceiver();
     TypeElement receiverType = receiver.getType();
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index f531071..58e1507 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
@@ -124,8 +125,8 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
-    super.verifyTypes(appView);
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
+    super.verifyTypes(appView, verifyTypesHelper);
     // DebugLocalWrite defines it's own verification of types but should be allowed to call super.
     if (!this.isDebugLocalWrite()) {
       assert src().getType().equals(getOutType());
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index ff7a054..0c6f609 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
@@ -225,7 +226,7 @@
   }
 
   @Override
-  public boolean verifyTypes(AppView<?> appView) {
+  public boolean verifyTypes(AppView<?> appView, VerifyTypesHelper verifyTypesHelper) {
     TypeElement type = getOutType();
     assert type.isClassType();
     assert type.asClassType().getClassType() == clazz || appView.options().testing.allowTypeErrors;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 47db073..6595192 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -172,7 +172,7 @@
     if (appView.options().testing.allowInvokeErrors) {
       return true;
     }
-    for (CfInstruction instruction : code.instructions) {
+    for (CfInstruction instruction : code.getInstructions()) {
       if (instruction instanceof CfInvoke) {
         CfInvoke invoke = (CfInvoke) instruction;
         if (invoke.getMethod().holder.isClassType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index f7e8a90..36c4111 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -779,7 +779,7 @@
       return canonicalPositions.getExceptionalExitPosition(
           appView.options().debug,
           () ->
-              code.instructions.stream()
+              code.getInstructions().stream()
                   .filter(insn -> insn instanceof CfPosition)
                   .map(insn -> ((CfPosition) insn).getPosition())
                   .collect(Collectors.toList()),
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index cd9bcd5..facf351 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -414,8 +414,8 @@
       Try tryRange, DexItemFactory factory, BiConsumer<DexType, Integer> fn) {
     TryHandler handler = code.handlers[tryRange.handlerIndex];
     for (TypeAddrPair pair : handler.pairs) {
-      fn.accept(pair.type, pair.addr);
-      if (pair.type == factory.throwableType) {
+      fn.accept(pair.getType(), pair.addr);
+      if (pair.getType() == factory.throwableType) {
         return;
       }
     }
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 32da612..100131c 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
@@ -9,6 +9,7 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
+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.Code;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.TypeChecker;
+import com.android.tools.r8.ir.analysis.VerifyTypesHelper;
 import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
 import com.android.tools.r8.ir.analysis.fieldaccess.FieldAccessAnalysis;
 import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
@@ -44,6 +46,7 @@
 import com.android.tools.r8.ir.desugar.D8NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter.Mode;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.Flavor;
@@ -310,7 +313,7 @@
       }
       this.devirtualizer =
           options.enableDevirtualization ? new Devirtualizer(appViewWithLiveness) : null;
-      this.typeChecker = new TypeChecker(appViewWithLiveness);
+      this.typeChecker = new TypeChecker(appViewWithLiveness, VerifyTypesHelper.create(appView));
       this.d8NestBasedAccessDesugaring = null;
       this.serviceLoaderRewriter =
           options.enableServiceLoaderRewriting
@@ -523,6 +526,9 @@
       return;
     }
     checkPrefixMerging(method);
+    if (!needsIRConversion(definition.getCode(), method)) {
+      return;
+    }
     if (options.isGeneratingClassFiles()
         || !(options.passthroughDexCode && definition.getCode().isDexCode())) {
       // We do not process in call graph order, so anything could be a leaf.
@@ -536,6 +542,35 @@
     }
   }
 
+  private boolean needsIRConversion(Code code, ProgramMethod method) {
+    if (options.isDesugaredLibraryCompilation()) {
+      // TODO(b/169035524): Create method for evaluating if a code object needs rewriting for
+      // library desugaring.
+      return true;
+    }
+    if (options.desugaredLibraryConfiguration != DesugaredLibraryConfiguration.empty()) {
+      // TODO(b/169035524): Create method for evaluating if a code object needs rewriting for
+      // library desugaring.
+      return true;
+    }
+
+    if (!options.cfToCfDesugar) {
+      return true;
+    }
+    if (method.getHolder().isInANest()) {
+      return true;
+    }
+
+    NeedsIRDesugarUseRegistry useRegistry =
+        new NeedsIRDesugarUseRegistry(appView, backportedMethodRewriter);
+    method.registerCodeReferences(useRegistry);
+
+    if (useRegistry.needsDesugaring()) {
+      return true;
+    }
+    return false;
+  }
+
   private void checkPrefixMerging(ProgramMethod method) {
     if (!appView.options().enableNeverMergePrefixes) {
       return;
@@ -1066,6 +1101,14 @@
           method.toSourceString(),
           logCode(options, method.getDefinition()));
     }
+    boolean didDesugar = desugar(method);
+    if (Log.ENABLED && didDesugar) {
+      Log.debug(
+          getClass(),
+          "Desugared code for %s:\n%s",
+          method.toSourceString(),
+          logCode(options, method.getDefinition()));
+    }
     if (options.testing.hookInIrConversion != null) {
       options.testing.hookInIrConversion.run();
     }
@@ -1081,6 +1124,21 @@
     return optimize(code, feedback, methodProcessor, methodProcessingId);
   }
 
+  private boolean desugar(ProgramMethod method) {
+    if (options.desugarState != DesugarState.ON) {
+      return false;
+    }
+    if (!method.getDefinition().getCode().isCfCode()) {
+      return false;
+    }
+    boolean didDesugar = false;
+    if (lambdaRewriter != null) {
+      AppInfoWithClassHierarchy appInfo = appView.appInfoForDesugaring();
+      didDesugar |= lambdaRewriter.desugarLambdas(method, appInfo) > 0;
+    }
+    return didDesugar;
+  }
+
   // TODO(b/140766440): Convert all sub steps an implementer of CodeOptimization
   private Timing optimize(
       IRCode code,
@@ -1129,13 +1187,6 @@
         || !appView.enableWholeProgramOptimizations()
         || !appView.appInfo().withLiveness().neverReprocess.contains(method.method);
 
-    if (!method.isProcessed() && lambdaRewriter != null) {
-      timing.begin("Desugar lambdas");
-      lambdaRewriter.desugarLambdas(code);
-      timing.end();
-      assert code.isConsistentSSA();
-    }
-
     if (lambdaMerger != null) {
       timing.begin("Merge lambdas");
       lambdaMerger.rewriteCode(code.context(), code, inliner, methodProcessor);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java b/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
new file mode 100644
index 0000000..9d88a1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
@@ -0,0 +1,97 @@
+// 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.ir.conversion;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
+import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
+
+class NeedsIRDesugarUseRegistry extends UseRegistry {
+
+  private boolean needsDesugarging = false;
+  private final AppView appView;
+  private final BackportedMethodRewriter backportedMethodRewriter;
+
+  public NeedsIRDesugarUseRegistry(
+      AppView appView, BackportedMethodRewriter backportedMethodRewriter) {
+    super(appView.dexItemFactory());
+    this.appView = appView;
+    this.backportedMethodRewriter = backportedMethodRewriter;
+  }
+
+  public boolean needsDesugaring() {
+    return needsDesugarging;
+  }
+
+  @Override
+  public void registerInitClass(DexType type) {}
+
+  @Override
+  public void registerInvokeVirtual(DexMethod method) {
+    if (backportedMethodRewriter.needsDesugaring(method)) {
+      needsDesugarging = true;
+    }
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod method) {}
+
+  @Override
+  public void registerInvokeStatic(DexMethod method) {
+    if (TwrCloseResourceRewriter.isSynthesizedCloseResourceMethod(method, appView)) {
+      needsDesugarging = true;
+    }
+
+    if (backportedMethodRewriter.needsDesugaring(method)) {
+      needsDesugarging = true;
+    }
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod method) {}
+
+  @Override
+  public void registerInvokeStatic(DexMethod method, boolean itf) {
+    if (itf) {
+      needsDesugarging = true;
+    }
+    registerInvokeStatic(method);
+  }
+
+  @Override
+  public void registerCallSite(DexCallSite callSite) {
+    super.registerCallSite(callSite);
+    needsDesugarging = true;
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod method) {}
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {}
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {}
+
+  @Override
+  public void registerNewInstance(DexType type) {}
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {}
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {}
+
+  @Override
+  public void registerTypeReference(DexType type) {}
+
+  @Override
+  public void registerInstanceOf(DexType type) {}
+}
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 8cc0ffa..da1a799 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
@@ -74,12 +74,15 @@
     // by the Android Platform build (which normally use an API level of 10000) there will be
     // no rewriting of backported methods. See b/147480264.
     this.enabled =
-        (appView.options().desugarState == DesugarState.ON
-                || appView.options().desugarState == DesugarState.ONLY_BACKPORT_STATICS)
+        appView.options().desugarState == DesugarState.ON
             && !this.rewritableMethods.isEmpty()
             && appView.options().minApiLevel <= AndroidApiLevel.LATEST.getLevel();
   }
 
+  public boolean needsDesugaring(DexMethod method) {
+    return rewritableMethods.getProvider(method) != null;
+  }
+
   public static List<DexMethod> generateListOfBackportedMethods(
       AndroidApp androidApp, InternalOptions options, ExecutorService executor) throws IOException {
     List<DexMethod> methods = new ArrayList<>();
@@ -121,11 +124,6 @@
       }
 
       InvokeMethod invoke = instruction.asInvokeMethod();
-      if (appView.options().desugarState == DesugarState.ONLY_BACKPORT_STATICS
-          && !invoke.isInvokeStatic()) {
-        continue;
-      }
-
       DexMethod invokedMethod = invoke.getInvokedMethod();
       MethodProvider provider = getMethodProviderOrNull(invokedMethod);
       if (provider != null) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
index 494831e..542c5d7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
@@ -36,6 +36,20 @@
 public class DesugaredLibraryConfiguration {
 
   public static final String FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX = "j$/";
+  public static final DesugaredLibraryConfiguration EMPTY_DESUGARED_LIBRARY_CONFIGURATION =
+      new DesugaredLibraryConfiguration(
+          AndroidApiLevel.B,
+          false,
+          FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX,
+          null,
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          ImmutableSet.of(),
+          ImmutableList.of(),
+          ImmutableList.of());
 
   // TODO(b/158632510): should use DexString, DexType, DexMethod or so on when possible.
   private final AndroidApiLevel requiredCompilationAPILevel;
@@ -73,19 +87,7 @@
   }
 
   public static DesugaredLibraryConfiguration empty() {
-    return new DesugaredLibraryConfiguration(
-        AndroidApiLevel.B,
-        false,
-        FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX,
-        null,
-        ImmutableMap.of(),
-        ImmutableMap.of(),
-        ImmutableMap.of(),
-        ImmutableMap.of(),
-        ImmutableMap.of(),
-        ImmutableSet.of(),
-        ImmutableList.of(),
-        ImmutableList.of());
+    return EMPTY_DESUGARED_LIBRARY_CONFIGURATION;
   }
 
   private DesugaredLibraryConfiguration(
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 2faa07a..7bb8d44 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
@@ -4,9 +4,21 @@
 
 package com.android.tools.r8.ir.desugar;
 
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.cf.code.CfStore;
+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.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -18,23 +30,22 @@
 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.TypeAnalysis;
 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.Instruction;
 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.utils.collections.SortedProgramMethodSet;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -44,6 +55,9 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.objectweb.asm.Opcodes;
 
 /**
  * Lambda desugaring rewriter.
@@ -105,41 +119,75 @@
    *
    * <p>NOTE: this method can be called concurrently for several different methods.
    */
-  public void desugarLambdas(IRCode code) {
-    Set<Value> affectedValues = Sets.newIdentityHashSet();
-    ProgramMethod context = code.context();
-    ListIterator<BasicBlock> blocks = code.listIterator();
-    while (blocks.hasNext()) {
-      BasicBlock block = blocks.next();
-      InstructionListIterator instructions = block.listIterator(code);
-      while (instructions.hasNext()) {
-        Instruction instruction = instructions.next();
-        if (instruction.isInvokeCustom()) {
-          InvokeCustom invoke = instruction.asInvokeCustom();
-          LambdaDescriptor descriptor = inferLambdaDescriptor(invoke.getCallSite(), context);
-          if (descriptor == LambdaDescriptor.MATCH_FAILED) {
-            continue;
+  public int desugarLambdas(ProgramMethod method, AppInfoWithClassHierarchy appInfo) {
+    return desugarLambdas(
+        method.getDefinition(),
+        callsite -> {
+          LambdaDescriptor descriptor = LambdaDescriptor.tryInfer(callsite, appInfo, method);
+          if (descriptor == null) {
+            return null;
           }
+          return getOrCreateLambdaClass(descriptor, method);
+        });
+  }
 
-          // We have a descriptor, get the lambda class. In D8, we synthesize the lambda classes
-          // during IR processing, and therefore we may need to create it now.
-          LambdaClass lambdaClass =
-              appView.enableWholeProgramOptimizations()
-                  ? getKnownLambdaClass(descriptor, context)
-                  : getOrCreateLambdaClass(descriptor, context);
-          assert lambdaClass != null;
-
-          // We rely on patch performing its work in a way which
-          // keeps both `instructions` and `blocks` iterators in
-          // valid state so that we can continue iteration.
-          patchInstruction(invoke, lambdaClass, code, blocks, instructions, affectedValues);
+  // Same as above, but where lambdas are always known to exist for the call sites.
+  public static int desugarLambdas(
+      DexEncodedMethod method, Function<DexCallSite, LambdaClass> callSites) {
+    CfCode code = method.getCode().asCfCode();
+    List<CfInstruction> instructions = code.getInstructions();
+    Supplier<List<CfInstruction>> lazyNewInstructions =
+        Suppliers.memoize(() -> new ArrayList<>(instructions));
+    int replaced = 0;
+    int maxTemp = 0;
+    int newInstructionsDelta = 0;
+    for (int i = 0; i < instructions.size(); i++) {
+      CfInstruction instruction = instructions.get(i);
+      if (instruction instanceof CfInvokeDynamic) {
+        LambdaClass lambdaClass = callSites.apply(((CfInvokeDynamic) instruction).getCallSite());
+        if (lambdaClass == null) {
+          continue;
         }
+        int newInstructionsIndex = i + newInstructionsDelta;
+        if (lambdaClass.isStateless()) {
+          CfFieldInstruction getStaticLambdaInstance =
+              new CfFieldInstruction(
+                  Opcodes.GETSTATIC, lambdaClass.lambdaField, lambdaClass.lambdaField);
+          lazyNewInstructions.get().set(newInstructionsIndex, getStaticLambdaInstance);
+        } else {
+          List<CfInstruction> replacement = new ArrayList<>();
+          int arguments = lambdaClass.descriptor.captures.size();
+          int temp = code.getMaxLocals();
+          for (int j = arguments - 1; j >= 0; j--) {
+            ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]);
+            replacement.add(new CfStore(type, temp));
+            temp += type.requiredRegisters();
+          }
+          maxTemp = Math.max(temp, maxTemp);
+          replacement.add(new CfNew(lambdaClass.type));
+          replacement.add(new CfStackInstruction(Opcode.Dup));
+          for (int j = 0; j < arguments; j++) {
+            ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]);
+            temp -= type.requiredRegisters();
+            replacement.add(new CfLoad(type, temp));
+          }
+          replacement.add(new CfInvoke(Opcodes.INVOKESPECIAL, lambdaClass.constructor, false));
+          List<CfInstruction> newInstructions = lazyNewInstructions.get();
+          newInstructions.remove(newInstructionsIndex);
+          newInstructions.addAll(newInstructionsIndex, replacement);
+          newInstructionsDelta += replacement.size() - 1;
+        }
+        ++replaced;
       }
     }
-    if (!affectedValues.isEmpty()) {
-      new TypeAnalysis(appView).narrowing(affectedValues);
+    if (maxTemp > 0) {
+      assert maxTemp > code.getMaxLocals();
+      code.setMaxLocals(maxTemp);
     }
-    assert code.isConsistentSSA();
+    if (replaced > 0) {
+      code.setInstructions(lazyNewInstructions.get());
+    }
+    return replaced;
   }
 
   /** Remove lambda deserialization methods. */
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index d4adf44..fbe8514 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -330,7 +330,8 @@
     }
     for (Phi phi : value.uniquePhiUsers()) {
       for (Value operand : phi.getOperands()) {
-        if (getEnumUnboxingCandidateOrNull(operand.getType()) != enumClass) {
+        if (!operand.getType().isNullType()
+            && getEnumUnboxingCandidateOrNull(operand.getType()) != enumClass) {
           markEnumAsUnboxable(Reason.INVALID_PHI, enumClass);
           return Reason.INVALID_PHI;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index c67c231..e09dab4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.ArrayAccess;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstanceGet;
@@ -55,6 +56,7 @@
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -148,182 +150,213 @@
     assert code.isConsistentSSABeforeTypesAreCorrect();
     Map<Instruction, DexType> convertedEnums = new IdentityHashMap<>();
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
-    InstructionListIterator iterator = code.instructionListIterator();
-    while (iterator.hasNext()) {
-      Instruction instruction = iterator.next();
-      // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
-      // counterpart.
-      if (instruction.isInvokeMethodWithReceiver()) {
-        InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
-        DexMethod invokedMethod = invokeMethod.getInvokedMethod();
-        DexType enumType = getEnumTypeOrNull(invokeMethod.getReceiver(), convertedEnums);
-        if (enumType != null) {
-          if (invokedMethod == factory.enumMembers.ordinalMethod
-              || invokedMethod == factory.enumMembers.hashCode) {
-            replaceEnumInvoke(
-                iterator, invokeMethod, ordinalUtilityMethod, m -> synthesizeOrdinalMethod());
-            continue;
-          } else if (invokedMethod == factory.enumMembers.equals) {
-            replaceEnumInvoke(
-                iterator, invokeMethod, equalsUtilityMethod, m -> synthesizeEqualsMethod());
-            continue;
-          } else if (invokedMethod == factory.enumMembers.compareTo) {
-            replaceEnumInvoke(
-                iterator, invokeMethod, compareToUtilityMethod, m -> synthesizeCompareToMethod());
-            continue;
-          } else if (invokedMethod == factory.enumMembers.nameMethod
-              || invokedMethod == factory.enumMembers.toString) {
-            DexMethod toStringMethod =
-                computeInstanceFieldUtilityMethod(enumType, factory.enumMembers.nameField);
-            iterator.replaceCurrentInstruction(
-                new InvokeStatic(
-                    toStringMethod, invokeMethod.outValue(), invokeMethod.arguments()));
-            continue;
-          } else if (invokedMethod == factory.objectMembers.getClass) {
-            assert !invokeMethod.hasOutValue() || !invokeMethod.outValue().hasAnyUsers();
-            replaceEnumInvoke(
-                iterator, invokeMethod, zeroCheckMethod, m -> synthesizeZeroCheckMethod());
-          }
-        }
-        // TODO(b/147860220): rewrite also other enum methods.
-      } else if (instruction.isInvokeStatic()) {
-        InvokeStatic invokeStatic = instruction.asInvokeStatic();
-        DexMethod invokedMethod = invokeStatic.getInvokedMethod();
-        if (invokedMethod == factory.enumMembers.valueOf
-            && invokeStatic.inValues().get(0).isConstClass()) {
-          DexType enumType =
-              invokeStatic.inValues().get(0).getConstInstruction().asConstClass().getValue();
-          if (enumsToUnbox.containsEnum(enumType)) {
-            DexMethod valueOfMethod = computeValueOfUtilityMethod(enumType);
-            Value outValue = invokeStatic.outValue();
-            Value rewrittenOutValue = null;
-            if (outValue != null) {
-              rewrittenOutValue = code.createValue(TypeElement.getInt());
-              affectedPhis.addAll(outValue.uniquePhiUsers());
+    ListIterator<BasicBlock> blocks = code.listIterator();
+    Value zeroConstValue = null;
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      zeroConstValue = fixNullsInBlockPhis(code, block, zeroConstValue);
+      InstructionListIterator iterator = block.listIterator(code);
+      while (iterator.hasNext()) {
+        Instruction instruction = iterator.next();
+        // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
+        // counterpart.
+        if (instruction.isInvokeMethodWithReceiver()) {
+          InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
+          DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+          DexType enumType = getEnumTypeOrNull(invokeMethod.getReceiver(), convertedEnums);
+          if (enumType != null) {
+            if (invokedMethod == factory.enumMembers.ordinalMethod
+                || invokedMethod == factory.enumMembers.hashCode) {
+              replaceEnumInvoke(
+                  iterator, invokeMethod, ordinalUtilityMethod, m -> synthesizeOrdinalMethod());
+              continue;
+            } else if (invokedMethod == factory.enumMembers.equals) {
+              replaceEnumInvoke(
+                  iterator, invokeMethod, equalsUtilityMethod, m -> synthesizeEqualsMethod());
+              continue;
+            } else if (invokedMethod == factory.enumMembers.compareTo) {
+              replaceEnumInvoke(
+                  iterator, invokeMethod, compareToUtilityMethod, m -> synthesizeCompareToMethod());
+              continue;
+            } else if (invokedMethod == factory.enumMembers.nameMethod
+                || invokedMethod == factory.enumMembers.toString) {
+              DexMethod toStringMethod =
+                  computeInstanceFieldUtilityMethod(enumType, factory.enumMembers.nameField);
+              iterator.replaceCurrentInstruction(
+                  new InvokeStatic(
+                      toStringMethod, invokeMethod.outValue(), invokeMethod.arguments()));
+              continue;
+            } else if (invokedMethod == factory.objectMembers.getClass) {
+              assert !invokeMethod.hasOutValue() || !invokeMethod.outValue().hasAnyUsers();
+              replaceEnumInvoke(
+                  iterator, invokeMethod, zeroCheckMethod, m -> synthesizeZeroCheckMethod());
             }
-            InvokeStatic invoke =
-                new InvokeStatic(
-                    valueOfMethod,
-                    rewrittenOutValue,
-                    Collections.singletonList(invokeStatic.inValues().get(1)));
-            iterator.replaceCurrentInstruction(invoke);
-            convertedEnums.put(invoke, enumType);
-            continue;
           }
-        } else if (invokedMethod == factory.javaLangSystemMethods.identityHashCode) {
-          assert invokeStatic.arguments().size() == 1;
-          Value argument = invokeStatic.getArgument(0);
-          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
-          if (enumType != null) {
-            invokeStatic.outValue().replaceUsers(argument);
-            iterator.removeOrReplaceByDebugLocalRead();
-          }
-        } else if (invokedMethod == factory.stringMembers.valueOf) {
-          assert invokeStatic.arguments().size() == 1;
-          Value argument = invokeStatic.getArgument(0);
-          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
-          if (enumType != null) {
-            DexMethod stringValueOfMethod = computeStringValueOfUtilityMethod(enumType);
-            iterator.replaceCurrentInstruction(
-                new InvokeStatic(
-                    stringValueOfMethod, invokeStatic.outValue(), invokeStatic.arguments()));
-            continue;
-          }
-        } else if (invokedMethod == factory.objectsMethods.requireNonNull) {
-          assert invokeStatic.arguments().size() == 1;
-          Value argument = invokeStatic.getArgument(0);
-          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
-          if (enumType != null) {
-            replaceEnumInvoke(
-                iterator, invokeStatic, zeroCheckMethod, m -> synthesizeZeroCheckMethod());
-          }
-        } else if (invokedMethod == factory.objectsMethods.requireNonNullWithMessage) {
-          assert invokeStatic.arguments().size() == 2;
-          Value argument = invokeStatic.getArgument(0);
-          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
-          if (enumType != null) {
-            replaceEnumInvoke(
-                iterator,
-                invokeStatic,
-                zeroCheckMessageMethod,
-                m -> synthesizeZeroCheckMessageMethod());
+          // TODO(b/147860220): rewrite also other enum methods.
+        } else if (instruction.isInvokeStatic()) {
+          InvokeStatic invokeStatic = instruction.asInvokeStatic();
+          DexMethod invokedMethod = invokeStatic.getInvokedMethod();
+          if (invokedMethod == factory.enumMembers.valueOf
+              && invokeStatic.inValues().get(0).isConstClass()) {
+            DexType enumType =
+                invokeStatic.inValues().get(0).getConstInstruction().asConstClass().getValue();
+            if (enumsToUnbox.containsEnum(enumType)) {
+              DexMethod valueOfMethod = computeValueOfUtilityMethod(enumType);
+              Value outValue = invokeStatic.outValue();
+              Value rewrittenOutValue = null;
+              if (outValue != null) {
+                rewrittenOutValue = code.createValue(TypeElement.getInt());
+                affectedPhis.addAll(outValue.uniquePhiUsers());
+              }
+              InvokeStatic invoke =
+                  new InvokeStatic(
+                      valueOfMethod,
+                      rewrittenOutValue,
+                      Collections.singletonList(invokeStatic.inValues().get(1)));
+              iterator.replaceCurrentInstruction(invoke);
+              convertedEnums.put(invoke, enumType);
+              continue;
+            }
+          } else if (invokedMethod == factory.javaLangSystemMethods.identityHashCode) {
+            assert invokeStatic.arguments().size() == 1;
+            Value argument = invokeStatic.getArgument(0);
+            DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+            if (enumType != null) {
+              invokeStatic.outValue().replaceUsers(argument);
+              iterator.removeOrReplaceByDebugLocalRead();
+            }
+          } else if (invokedMethod == factory.stringMembers.valueOf) {
+            assert invokeStatic.arguments().size() == 1;
+            Value argument = invokeStatic.getArgument(0);
+            DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+            if (enumType != null) {
+              DexMethod stringValueOfMethod = computeStringValueOfUtilityMethod(enumType);
+              iterator.replaceCurrentInstruction(
+                  new InvokeStatic(
+                      stringValueOfMethod, invokeStatic.outValue(), invokeStatic.arguments()));
+              continue;
+            }
+          } else if (invokedMethod == factory.objectsMethods.requireNonNull) {
+            assert invokeStatic.arguments().size() == 1;
+            Value argument = invokeStatic.getArgument(0);
+            DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+            if (enumType != null) {
+              replaceEnumInvoke(
+                  iterator, invokeStatic, zeroCheckMethod, m -> synthesizeZeroCheckMethod());
+            }
+          } else if (invokedMethod == factory.objectsMethods.requireNonNullWithMessage) {
+            assert invokeStatic.arguments().size() == 2;
+            Value argument = invokeStatic.getArgument(0);
+            DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+            if (enumType != null) {
+              replaceEnumInvoke(
+                  iterator,
+                  invokeStatic,
+                  zeroCheckMessageMethod,
+                  m -> synthesizeZeroCheckMessageMethod());
+            }
           }
         }
-      }
-      if (instruction.isStaticGet()) {
-        StaticGet staticGet = instruction.asStaticGet();
-        DexType holder = staticGet.getField().holder;
-        if (enumsToUnbox.containsEnum(holder)) {
-          if (staticGet.outValue() == null) {
-            iterator.removeOrReplaceByDebugLocalRead();
-            continue;
+        if (instruction.isStaticGet()) {
+          StaticGet staticGet = instruction.asStaticGet();
+          DexType holder = staticGet.getField().holder;
+          if (enumsToUnbox.containsEnum(holder)) {
+            if (staticGet.outValue() == null) {
+              iterator.removeOrReplaceByDebugLocalRead();
+              continue;
+            }
+            EnumValueInfoMap enumValueInfoMap = enumsToUnbox.getEnumValueInfoMap(holder);
+            assert enumValueInfoMap != null;
+            affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
+            EnumValueInfo enumValueInfo = enumValueInfoMap.getEnumValueInfo(staticGet.getField());
+            if (enumValueInfo == null && staticGet.getField().name == factory.enumValuesFieldName) {
+              utilityMethods.computeIfAbsent(
+                  valuesUtilityMethod, m -> synthesizeValuesUtilityMethod());
+              DexField fieldValues = createValuesField(holder);
+              utilityFields.computeIfAbsent(fieldValues, this::computeValuesEncodedField);
+              DexMethod methodValues = createValuesMethod(holder);
+              utilityMethods.computeIfAbsent(
+                  methodValues,
+                  m -> computeValuesEncodedMethod(m, fieldValues, enumValueInfoMap.size()));
+              Value rewrittenOutValue =
+                  code.createValue(
+                      ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
+              InvokeStatic invoke =
+                  new InvokeStatic(methodValues, rewrittenOutValue, ImmutableList.of());
+              iterator.replaceCurrentInstruction(invoke);
+              convertedEnums.put(invoke, holder);
+            } else {
+              // Replace by ordinal + 1 for null check (null is 0).
+              assert enumValueInfo != null
+                  : "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
+              ConstNumber intConstant = code.createIntConstant(enumValueInfo.convertToInt());
+              iterator.replaceCurrentInstruction(intConstant);
+              convertedEnums.put(intConstant, holder);
+            }
           }
-          EnumValueInfoMap enumValueInfoMap = enumsToUnbox.getEnumValueInfoMap(holder);
-          assert enumValueInfoMap != null;
-          affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
-          EnumValueInfo enumValueInfo = enumValueInfoMap.getEnumValueInfo(staticGet.getField());
-          if (enumValueInfo == null && staticGet.getField().name == factory.enumValuesFieldName) {
-            utilityMethods.computeIfAbsent(
-                valuesUtilityMethod, m -> synthesizeValuesUtilityMethod());
-            DexField fieldValues = createValuesField(holder);
-            utilityFields.computeIfAbsent(fieldValues, this::computeValuesEncodedField);
-            DexMethod methodValues = createValuesMethod(holder);
-            utilityMethods.computeIfAbsent(
-                methodValues,
-                m -> computeValuesEncodedMethod(m, fieldValues, enumValueInfoMap.size()));
+        }
+
+        if (instruction.isInstanceGet()) {
+          InstanceGet instanceGet = instruction.asInstanceGet();
+          DexType holder = instanceGet.getField().holder;
+          if (enumsToUnbox.containsEnum(holder)) {
+            DexMethod fieldMethod = computeInstanceFieldMethod(instanceGet.getField());
             Value rewrittenOutValue =
                 code.createValue(
-                    ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
+                    TypeElement.fromDexType(
+                        fieldMethod.proto.returnType, Nullability.maybeNull(), appView));
             InvokeStatic invoke =
-                new InvokeStatic(methodValues, rewrittenOutValue, ImmutableList.of());
+                new InvokeStatic(
+                    fieldMethod, rewrittenOutValue, ImmutableList.of(instanceGet.object()));
             iterator.replaceCurrentInstruction(invoke);
-            convertedEnums.put(invoke, holder);
-          } else {
-            // Replace by ordinal + 1 for null check (null is 0).
-            assert enumValueInfo != null
-                : "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
-            ConstNumber intConstant = code.createIntConstant(enumValueInfo.convertToInt());
-            iterator.replaceCurrentInstruction(intConstant);
-            convertedEnums.put(intConstant, holder);
+            if (enumsToUnbox.containsEnum(instanceGet.getField().type)) {
+              convertedEnums.put(invoke, instanceGet.getField().type);
+            }
           }
         }
-      }
 
-      if (instruction.isInstanceGet()) {
-        InstanceGet instanceGet = instruction.asInstanceGet();
-        DexType holder = instanceGet.getField().holder;
-        if (enumsToUnbox.containsEnum(holder)) {
-          DexMethod fieldMethod = computeInstanceFieldMethod(instanceGet.getField());
-          Value rewrittenOutValue =
-              code.createValue(
-                  TypeElement.fromDexType(
-                      fieldMethod.proto.returnType, Nullability.maybeNull(), appView));
-          InvokeStatic invoke =
-              new InvokeStatic(
-                  fieldMethod, rewrittenOutValue, ImmutableList.of(instanceGet.object()));
-          iterator.replaceCurrentInstruction(invoke);
-          if (enumsToUnbox.containsEnum(instanceGet.getField().type)) {
-            convertedEnums.put(invoke, instanceGet.getField().type);
+        // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
+        if (instruction.isArrayAccess()) {
+          ArrayAccess arrayAccess = instruction.asArrayAccess();
+          DexType enumType = getEnumTypeOrNull(arrayAccess);
+          if (enumType != null) {
+            instruction = arrayAccess.withMemberType(MemberType.INT);
+            iterator.replaceCurrentInstruction(instruction);
+            convertedEnums.put(instruction, enumType);
           }
+          assert validateArrayAccess(arrayAccess);
         }
       }
-
-      // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
-      if (instruction.isArrayAccess()) {
-        ArrayAccess arrayAccess = instruction.asArrayAccess();
-        DexType enumType = getEnumTypeOrNull(arrayAccess);
-        if (enumType != null) {
-          instruction = arrayAccess.withMemberType(MemberType.INT);
-          iterator.replaceCurrentInstruction(instruction);
-          convertedEnums.put(instruction, enumType);
-        }
-        assert validateArrayAccess(arrayAccess);
-      }
     }
     assert code.isConsistentSSABeforeTypesAreCorrect();
     return affectedPhis;
   }
 
+  private Value fixNullsInBlockPhis(IRCode code, BasicBlock block, Value zeroConstValue) {
+    for (Phi phi : block.getPhis()) {
+      if (getEnumTypeOrNull(phi.getType()) != null) {
+        for (int i = 0; i < phi.getOperands().size(); i++) {
+          Value operand = phi.getOperand(i);
+          if (operand.getType().isNullType()) {
+            if (zeroConstValue == null) {
+              zeroConstValue = insertConstZero(code);
+            }
+            phi.replaceOperandAt(i, zeroConstValue);
+          }
+        }
+      }
+    }
+    return zeroConstValue;
+  }
+
+  private Value insertConstZero(IRCode code) {
+    InstructionListIterator iterator = code.entryBlock().listIterator(code);
+    while (iterator.hasNext() && iterator.peekNext().isArgument()) {
+      iterator.next();
+    }
+    return iterator.insertConstNumberInstruction(code, appView.options(), 0, TypeElement.getInt());
+  }
+
   private DexMethod computeInstanceFieldMethod(DexField field) {
     EnumInstanceFieldKnownData enumFieldKnownData =
         unboxedEnumsInstanceFieldData.getInstanceFieldData(field.holder, field);
@@ -362,6 +395,10 @@
     if (type.isInt()) {
       return convertedEnums.get(receiver.definition);
     }
+    return getEnumTypeOrNull(type);
+  }
+
+  private DexType getEnumTypeOrNull(TypeElement type) {
     if (!type.isClassType()) {
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index a0b1328..b9fc4cb 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardClassFilter;
 import com.android.tools.r8.utils.ThreadUtils;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
@@ -143,7 +144,8 @@
     if (staticValue instanceof DexItemBasedValueString) {
       DexItemBasedValueString cnst = (DexItemBasedValueString) staticValue;
       DexString replacement =
-          cnst.getNameComputationInfo().computeNameFor(cnst.getValue(), appView, lens);
+          cnst.getNameComputationInfo()
+              .computeNameFor(cnst.getValue(), appView, appView.graphLens(), lens);
       encodedField.setStaticValue(new DexValueString(replacement));
     }
   }
@@ -158,7 +160,8 @@
         if (instruction.isDexItemBasedConstString()) {
           DexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
           DexString replacement =
-              cnst.getNameComputationInfo().computeNameFor(cnst.getItem(), appView, lens);
+              cnst.getNameComputationInfo()
+                  .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens);
           ConstString constString = new ConstString(cnst.AA, replacement);
           constString.setOffset(instruction.getOffset());
           instructions[i] = constString;
@@ -166,16 +169,24 @@
       }
     } else {
       assert code.isCfCode();
-      List<CfInstruction> instructions = code.asCfCode().instructions;
+      List<CfInstruction> instructions = code.asCfCode().getInstructions();
+      List<CfInstruction> newInstructions = null;
       for (int i = 0; i < instructions.size(); ++i) {
         CfInstruction instruction = instructions.get(i);
         if (instruction.isDexItemBasedConstString()) {
           CfDexItemBasedConstString cnst = instruction.asDexItemBasedConstString();
           DexString replacement =
-              cnst.getNameComputationInfo().computeNameFor(cnst.getItem(), appView, lens);
-          instructions.set(i, new CfConstString(replacement));
+              cnst.getNameComputationInfo()
+                  .computeNameFor(cnst.getItem(), appView, appView.graphLens(), lens);
+          if (newInstructions == null) {
+            newInstructions = new ArrayList<>(instructions);
+          }
+          newInstructions.set(i, new CfConstString(replacement));
         }
       }
+      if (newInstructions != null) {
+        code.asCfCode().setInstructions(newInstructions);
+      }
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
index a53864f..67e574c 100644
--- a/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
+++ b/src/main/java/com/android/tools/r8/naming/dexitembasedstring/NameComputationInfo.java
@@ -7,23 +7,28 @@
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.naming.NamingLens;
 
 public abstract class NameComputationInfo<T extends DexReference> {
 
   public final DexString computeNameFor(
-      DexReference reference, DexDefinitionSupplier definitions, NamingLens namingLens) {
+      DexReference reference,
+      DexDefinitionSupplier definitions,
+      GraphLens graphLens,
+      NamingLens namingLens) {
+    DexReference rewritten = graphLens.lookupReference(reference);
     if (needsToComputeName()) {
       if (isFieldNameComputationInfo()) {
         return asFieldNameComputationInfo()
-            .internalComputeNameFor(reference.asDexField(), definitions, namingLens);
+            .internalComputeNameFor(rewritten.asDexField(), definitions, namingLens);
       }
       if (isClassNameComputationInfo()) {
         return asClassNameComputationInfo()
-            .internalComputeNameFor(reference.asDexType(), definitions, namingLens);
+            .internalComputeNameFor(rewritten.asDexType(), definitions, namingLens);
       }
     }
-    return namingLens.lookupName(reference, definitions.dexItemFactory());
+    return namingLens.lookupName(rewritten, definitions.dexItemFactory());
   }
 
   abstract DexString internalComputeNameFor(
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 0afccd0..a210ef7 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -10,15 +10,6 @@
 import static com.android.tools.r8.shaking.AnnotationRemover.shouldKeepAnnotation;
 
 import com.android.tools.r8.Diagnostic;
-import com.android.tools.r8.cf.code.CfFieldInstruction;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfInvokeDynamic;
-import com.android.tools.r8.cf.code.CfLoad;
-import com.android.tools.r8.cf.code.CfNew;
-import com.android.tools.r8.cf.code.CfStackInstruction;
-import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
-import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
@@ -85,7 +76,6 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.LambdaClass;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
@@ -145,7 +135,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import org.objectweb.asm.Opcodes;
 
 /**
  * Approximates the runtime dependencies for the given set of roots.
@@ -3190,50 +3179,7 @@
   private void rewriteLambdaCallSites(
       DexEncodedMethod method, Map<DexCallSite, LambdaClass> callSites) {
     assert !callSites.isEmpty();
-    CfCode code = method.getCode().asCfCode();
-    List<CfInstruction> instructions = code.instructions;
-    int replaced = 0;
-    int maxTemp = 0;
-    for (int i = 0; i < instructions.size(); i++) {
-      CfInstruction instruction = instructions.get(i);
-      if (instruction instanceof CfInvokeDynamic) {
-        LambdaClass lambdaClass = callSites.get(((CfInvokeDynamic) instruction).getCallSite());
-        if (lambdaClass == null) {
-          continue;
-        }
-        if (lambdaClass.isStateless()) {
-          CfFieldInstruction getStaticLambdaInstance =
-              new CfFieldInstruction(
-                  Opcodes.GETSTATIC, lambdaClass.lambdaField, lambdaClass.lambdaField);
-          instructions.set(i, getStaticLambdaInstance);
-        } else {
-          List<CfInstruction> replacement = new ArrayList<>();
-          int arguments = lambdaClass.descriptor.captures.size();
-          int temp = code.getMaxLocals();
-          for (int j = arguments - 1; j >= 0; j--) {
-            ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]);
-            replacement.add(new CfStore(type, temp));
-            temp += type.requiredRegisters();
-          }
-          maxTemp = Math.max(temp, maxTemp);
-          replacement.add(new CfNew(lambdaClass.type));
-          replacement.add(new CfStackInstruction(Opcode.Dup));
-          for (int j = 0; j < arguments; j++) {
-            ValueType type = ValueType.fromDexType(lambdaClass.descriptor.captures.values[j]);
-            temp -= type.requiredRegisters();
-            replacement.add(new CfLoad(type, temp));
-          }
-          replacement.add(new CfInvoke(Opcodes.INVOKESPECIAL, lambdaClass.constructor, false));
-          instructions.remove(i);
-          instructions.addAll(i, replacement);
-        }
-        ++replaced;
-      }
-    }
-    if (maxTemp > 0) {
-      assert maxTemp > code.getMaxLocals();
-      code.setMaxLocals(maxTemp);
-    }
+    int replaced = LambdaRewriter.desugarLambdas(method, callSites::get);
     assert replaced == callSites.size();
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java b/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
index 67cd3b8..a5fbd32 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
@@ -49,8 +49,12 @@
     }
   }
 
+  public boolean contains(DexType type) {
+    return mainDexClasses.contains(type);
+  }
+
   public boolean contains(DexProgramClass clazz) {
-    return mainDexClasses.contains(clazz.getType());
+    return contains(clazz.getType());
   }
 
   public boolean containsAnyOf(Iterable<DexProgramClass> classes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java b/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
index 973691f..91f9bbc 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
@@ -47,7 +48,10 @@
       assert clazz != null;
       consumer.accept(type);
       // Super and interfaces are live, no need to add them.
-      traceAnnotationsDirectDependencies(clazz.annotations());
+      if (!DexAnnotation.hasSynthesizedClassAnnotation(
+          clazz.annotations(), appInfo.dexItemFactory())) {
+        traceAnnotationsDirectDependencies(clazz.annotations());
+      }
       clazz.forEachField(field -> consumer.accept(field.field.type));
       clazz.forEachProgramMethodMatching(
           definition -> {
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 380c2dc..93d1248 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -3,34 +3,106 @@
 // 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.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.shaking.MainDexClasses;
+import java.util.Comparator;
+import java.util.Set;
 
 /**
- * A synthesizing context is the input type and origin that gives rise to a synthetic item.
+ * A synthesizing context is a description of the context that gives rise to a synthetic item.
  *
  * <p>Note that a context can only itself be a synthetic item if it was provided as an input that
- * was marked as synthetic already, in which case it is its own context. In other words,
- *
- * <pre>
- *   for any synthetic item, I:
- *     context(I) == holder(I)  iff  I is a synthetic input
- * </pre>
+ * was marked as synthetic already, in which case the context consists of the synthetic input type
+ * as well as the original synthesizing context type specified in it synthesis annotation.
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
-class SynthesizingContext {
-  final DexType type;
-  final Origin origin;
+class SynthesizingContext implements Comparable<SynthesizingContext> {
 
-  SynthesizingContext(DexType type, Origin origin) {
-    this.type = type;
-    this.origin = origin;
+  // The synthesizing context is the type used for ensuring a hygienic placement of a synthetic.
+  // Thus this type will potentially be used as the prefix of a synthetic class.
+  private final DexType synthesizingContextType;
+
+  // The input context is the program input type that is the actual context of a synthetic.
+  // In particular, if the synthetic type is itself a program input, then it will be its own
+  // input context but it will have a distinct synthesizing context (encoded in its annotation).
+  private final DexType inputContextType;
+  private final Origin inputContextOrigin;
+
+  static SynthesizingContext fromNonSyntheticInputClass(DexProgramClass clazz) {
+    // A context that is itself non-synthetic is the single context, thus both the input context
+    // and synthesizing context coincide.
+    return new SynthesizingContext(clazz.type, clazz.type, clazz.origin);
+  }
+
+  static SynthesizingContext fromSyntheticInputClass(
+      DexProgramClass clazz, DexType synthesizingContextType) {
+    assert synthesizingContextType != null;
+    // A context that is itself synthetic must denote a synthesizing context from which to ensure
+    // hygiene. This synthesizing context type is encoded on the synthetic for intermediate builds.
+    return new SynthesizingContext(synthesizingContextType, clazz.type, clazz.origin);
+  }
+
+  private SynthesizingContext(
+      DexType synthesizingContextType, DexType inputContextType, Origin inputContextOrigin) {
+    this.synthesizingContextType = synthesizingContextType;
+    this.inputContextType = inputContextType;
+    this.inputContextOrigin = inputContextOrigin;
+  }
+
+  @Override
+  public int compareTo(SynthesizingContext other) {
+    return Comparator
+        // The first item to compare is the synthesizing context type. This is the type used to
+        // choose the context prefix for items.
+        .comparing(SynthesizingContext::getSynthesizingContextType, DexType::slowCompareTo)
+        // To ensure that equals coincides with compareTo == 0, we then compare 'type'.
+        .thenComparing(c -> c.inputContextType, DexType::slowCompareTo)
+        .compare(this, other);
+  }
+
+  Origin getInputContextOrigin() {
+    return inputContextOrigin;
+  }
+
+  DexType createHygienicType(int 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 rewritten = lens.lookupType(type);
-    return rewritten == type ? this : new SynthesizingContext(type, origin);
+    DexType rewrittenInputeContextType = lens.lookupType(inputContextType);
+    DexType rewrittenSynthesizingContextType = lens.lookupType(synthesizingContextType);
+    return rewrittenInputeContextType == inputContextType
+            && rewrittenSynthesizingContextType == synthesizingContextType
+        ? this
+        : new SynthesizingContext(
+            rewrittenSynthesizingContextType, rewrittenInputeContextType, inputContextOrigin);
+  }
+
+  DexType getSynthesizingContextType() {
+    return synthesizingContextType;
+  }
+
+  void addIfDerivedFromMainDexClass(
+      DexProgramClass externalSyntheticClass,
+      MainDexClasses mainDexClasses,
+      Set<DexType> allMainDexTypes,
+      Set<DexType> derivedMainDexTypesToIgnore) {
+    // 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);
+    }
   }
 }
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 ad04a8d..1091d09 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -39,7 +39,7 @@
   SyntheticClassBuilder(DexType type, SynthesizingContext context, DexItemFactory factory) {
     this.factory = factory;
     this.type = type;
-    this.origin = context.origin;
+    this.origin = context.getInputContextOrigin();
     this.superType = factory.objectType;
   }
 
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 daf936d..1d037cc 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -4,12 +4,10 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.origin.Origin;
 import com.google.common.hash.HashCode;
 
 /**
- * Base type for the defintion of a synthetic item.
+ * Base type for the definition of a synthetic item.
  *
  * <p>This class is internal to the synthetic items collection, thus package-protected.
  */
@@ -26,14 +24,6 @@
     return context;
   }
 
-  DexType getContextType() {
-    return context.type;
-  }
-
-  Origin getContextOrigin() {
-    return context.origin;
-  }
-
   abstract DexProgramClass getHolder();
 
   abstract HashCode computeHash();
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 2996794..31a4afd 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -21,6 +21,7 @@
 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;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -29,6 +30,7 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeSet;
 import java.util.function.Predicate;
 
@@ -103,19 +105,22 @@
     Builder lensBuilder = NestedGraphLens.builder();
     List<DexProgramClass> newProgramClasses = new ArrayList<>();
     List<DexProgramClass> finalSyntheticClasses = new ArrayList<>();
+    Set<DexType> derivedMainDexTypesToIgnore = Sets.newIdentityHashSet();
     buildLensAndProgram(
         application,
         equivalences,
         syntheticItems::containsKey,
         mainDexClasses,
         lensBuilder,
-        options.itemFactory,
+        options,
         newProgramClasses,
-        finalSyntheticClasses);
+        finalSyntheticClasses,
+        derivedMainDexTypesToIgnore);
 
     newProgramClasses.addAll(finalSyntheticClasses);
 
-    handleSynthesizedClassMapping(finalSyntheticClasses, application, options, mainDexClasses);
+    handleSynthesizedClassMapping(
+        finalSyntheticClasses, application, options, mainDexClasses, derivedMainDexTypesToIgnore);
 
     DexApplication app = application.builder().replaceProgramClasses(newProgramClasses).build();
 
@@ -132,10 +137,9 @@
   }
 
   private boolean verifyNoNestedSynthetics() {
+    // Check that a context is never itself synthetic class.
     for (SyntheticReference item : syntheticItems.values()) {
-      // Check that a context is never a synthetic unless it is an input, thus its own context.
-      assert item.getHolder() == item.getContextType()
-          || !syntheticItems.containsKey(item.getContextType());
+      assert !syntheticItems.containsKey(item.getContext().getSynthesizingContextType());
     }
     return true;
   }
@@ -144,12 +148,14 @@
       List<DexProgramClass> finalSyntheticClasses,
       DexApplication application,
       InternalOptions options,
-      MainDexClasses mainDexClasses) {
-    boolean includeSynthesizedClassMappingInOutput = options.intermediate && !options.cfToCfDesugar;
+      MainDexClasses mainDexClasses,
+      Set<DexType> derivedMainDexTypesToIgnore) {
+    boolean includeSynthesizedClassMappingInOutput = shouldAnnotateSynthetics(options);
     if (includeSynthesizedClassMappingInOutput) {
       updateSynthesizedClassMapping(application, finalSyntheticClasses);
     }
-    updateMainDexListWithSynthesizedClassMap(application, mainDexClasses);
+    updateMainDexListWithSynthesizedClassMap(
+        application, mainDexClasses, derivedMainDexTypesToIgnore);
     if (!includeSynthesizedClassMappingInOutput) {
       clearSynthesizedClassMapping(application);
     }
@@ -194,7 +200,9 @@
   }
 
   private void updateMainDexListWithSynthesizedClassMap(
-      DexApplication application, MainDexClasses mainDexClasses) {
+      DexApplication application,
+      MainDexClasses mainDexClasses,
+      Set<DexType> derivedMainDexTypesToIgnore) {
     if (mainDexClasses.isEmpty()) {
       return;
     }
@@ -208,10 +216,12 @@
                 DexAnnotation.readAnnotationSynthesizedClassMap(
                     programClass, application.dexItemFactory);
             for (DexType type : derived) {
-              DexProgramClass syntheticClass =
-                  DexProgramClass.asProgramClassOrNull(application.definitionFor(type));
-              if (syntheticClass != null) {
-                newMainDexClasses.add(syntheticClass);
+              if (!derivedMainDexTypesToIgnore.contains(type)) {
+                DexProgramClass syntheticClass =
+                    DexProgramClass.asProgramClassOrNull(application.definitionFor(type));
+                if (syntheticClass != null) {
+                  newMainDexClasses.add(syntheticClass);
+                }
               }
             }
           }
@@ -232,9 +242,11 @@
       Predicate<DexType> isSyntheticType,
       MainDexClasses mainDexClasses,
       Builder lensBuilder,
-      DexItemFactory factory,
+      InternalOptions options,
       List<DexProgramClass> normalClasses,
-      List<DexProgramClass> newSyntheticClasses) {
+      List<DexProgramClass> newSyntheticClasses,
+      Set<DexType> derivedMainDexTypesToIgnore) {
+    DexItemFactory factory = options.itemFactory;
 
     for (DexProgramClass clazz : app.classes()) {
       if (!isSyntheticType.test(clazz.type)) {
@@ -242,43 +254,78 @@
       }
     }
 
+    // TODO(b/168584485): Remove this once class-mapping support is removed.
+    Set<DexType> derivedMainDexTypes = Sets.newIdentityHashSet();
+    mainDexClasses.forEach(
+        mainDexType -> {
+          derivedMainDexTypes.add(mainDexType);
+          DexProgramClass mainDexClass =
+              DexProgramClass.asProgramClassOrNull(app.definitionFor(mainDexType));
+          if (mainDexClass != null) {
+            derivedMainDexTypes.addAll(
+                DexAnnotation.readAnnotationSynthesizedClassMap(mainDexClass, options.itemFactory));
+          }
+        });
+
     syntheticMethodGroups.forEach(
         (syntheticType, syntheticGroup) -> {
-          SyntheticMethodDefinition firstMember = syntheticGroup.getRepresentative();
-          SynthesizingContext context = firstMember.getContext();
+          SyntheticMethodDefinition representative = syntheticGroup.getRepresentative();
+          SynthesizingContext context = representative.getContext();
           SyntheticClassBuilder builder =
               new SyntheticClassBuilder(syntheticType, context, factory);
           // TODO(b/158159959): Support grouping multiple methods per synthetic class.
           builder.addMethod(
               methodBuilder -> {
-                DexEncodedMethod definition = firstMember.getMethod().getDefinition();
+                DexEncodedMethod definition = representative.getMethod().getDefinition();
                 methodBuilder
                     .setAccessFlags(definition.accessFlags)
                     .setProto(definition.getProto())
                     .setCode(m -> definition.getCode());
               });
           DexProgramClass externalSyntheticClass = builder.build();
+          if (shouldAnnotateSynthetics(options)) {
+            externalSyntheticClass.setAnnotations(
+                externalSyntheticClass
+                    .annotations()
+                    .getWithAddedOrReplaced(
+                        DexAnnotation.createAnnotationSynthesizedClass(
+                            context.getSynthesizingContextType(), factory)));
+          }
           assert externalSyntheticClass.getMethodCollection().size() == 1;
           DexEncodedMethod externalSyntheticMethod =
               externalSyntheticClass.methods().iterator().next();
           newSyntheticClasses.add(externalSyntheticClass);
           for (SyntheticMethodDefinition member : syntheticGroup.getMembers()) {
-            lensBuilder.map(
-                member.getMethod().getHolder().getType(), externalSyntheticClass.getType());
-            lensBuilder.map(member.getMethod().getReference(), externalSyntheticMethod.method);
-            DexType memberContext = member.getContextType();
+            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(app.definitionFor(memberContext));
+                DexProgramClass.asProgramClassOrNull(
+                    app.definitionFor(member.getContext().getSynthesizingContextType()));
             if (from != null) {
               externalSyntheticClass.addSynthesizedFrom(from);
-              if (mainDexClasses.contains(from)) {
-                mainDexClasses.add(externalSyntheticClass);
-              }
             }
           }
         });
   }
 
+  private static boolean shouldAnnotateSynthetics(InternalOptions options) {
+    // Only intermediate builds have annotated synthetics to allow later sharing.
+    // This is currently also disabled on CF to CF desugaring to avoid missing class references to
+    // the annotated classes.
+    // TODO(b/147485959): Find an alternative encoding for synthetics to avoid missing-class refs.
+    // TODO(b/168584485): Remove support for main-dex tracing with the class-map annotation.
+    return options.intermediate && !options.cfToCfDesugar;
+  }
+
   private static <T extends SyntheticDefinition & Comparable<T>>
       Map<DexType, EquivalenceGroup<T>> computeActualEquivalences(
           Map<HashCode, List<T>> potentialEquivalences, DexItemFactory factory) {
@@ -298,13 +345,15 @@
                 // The member becomes a new singleton group.
                 // TODO(b/158159959): Consider checking for sub-groups of matching members.
                 groupsPerContext
-                    .computeIfAbsent(member.getContextType(), k -> new ArrayList<>())
+                    .computeIfAbsent(
+                        member.getContext().getSynthesizingContextType(), k -> new ArrayList<>())
                     .add(new EquivalenceGroup<>(member));
               }
             }
           }
           groupsPerContext
-              .computeIfAbsent(representative.getContextType(), k -> new ArrayList<>())
+              .computeIfAbsent(
+                  representative.getContext().getSynthesizingContextType(), k -> new ArrayList<>())
               .add(new EquivalenceGroup<>(representative, group));
         });
     Map<DexType, EquivalenceGroup<T>> equivalences = new IdentityHashMap<>();
@@ -312,7 +361,9 @@
         (context, groups) -> {
           groups.sort(EquivalenceGroup::compareTo);
           for (int i = 0; i < groups.size(); i++) {
-            equivalences.put(createExternalType(context, i, factory), groups.get(i));
+            EquivalenceGroup<T> group = groups.get(i);
+            DexType representativeType = createExternalType(context, i, factory);
+            equivalences.put(representativeType, group);
           }
         });
     return equivalences;
@@ -355,10 +406,14 @@
     List<SyntheticMethodDefinition> methods = new ArrayList<>(syntheticItems.size());
     for (SyntheticReference reference : syntheticItems.values()) {
       SyntheticDefinition definition = reference.lookupDefinition(finalApp::definitionFor);
-      assert definition != null;
-      assert definition instanceof SyntheticMethodDefinition;
-      if (definition != null && definition instanceof SyntheticMethodDefinition) {
-        methods.add(((SyntheticMethodDefinition) definition));
+      if (definition == null || !(definition instanceof SyntheticMethodDefinition)) {
+        // We expect pruned definitions to have been removed.
+        assert false;
+        continue;
+      }
+      SyntheticMethodDefinition method = (SyntheticMethodDefinition) definition;
+      if (SyntheticMethodBuilder.isValidSyntheticMethod(method.getMethod().getDefinition())) {
+        methods.add(method);
       }
     }
     return methods;
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 ef45975..6f5c49d 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -4,9 +4,14 @@
 package com.android.tools.r8.synthesis;
 
 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;
@@ -46,7 +51,7 @@
   public 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";
+  public static final String INTERNAL_SYNTHETIC_METHOD_PREFIX = "m";
 
   public static boolean verifyNotInternalSynthetic(DexType type) {
     assert !type.toDescriptorString().contains(SyntheticItems.INTERNAL_SYNTHETIC_CLASS_SEPARATOR);
@@ -58,13 +63,15 @@
 
   /**
    * Thread safe collection of synthesized classes that are not yet committed to the application.
-   * TODO(b/158159959): Remove legacy support.
+   *
+   * <p>TODO(b/158159959): Remove legacy support.
    */
   private final Map<DexType, DexProgramClass> legacyPendingClasses = new ConcurrentHashMap<>();
 
   /**
-   * Immutable set of synthetic types in the application (eg, committed). TODO(b/158159959): Remove
-   * legacy support.
+   * Immutable set of synthetic types in the application (eg, committed).
+   *
+   * <p>TODO(b/158159959): Remove legacy support.
    */
   private final ImmutableSet<DexType> legacySyntheticTypes;
 
@@ -76,8 +83,9 @@
   private final ImmutableMap<DexType, SyntheticReference> nonLecacySyntheticItems;
 
   // Only for use from initial AppInfo/AppInfoWithClassHierarchy create functions. */
-  public static SyntheticItems createInitialSyntheticItems() {
-    return new SyntheticItems(0, ImmutableSet.of(), ImmutableMap.of());
+  public static CommittedItems createInitialSyntheticItems(DexApplication application) {
+    return new CommittedItems(
+        0, application, ImmutableSet.of(), ImmutableMap.of(), ImmutableList.of());
   }
 
   // Only for conversion to a mutable synthetic items collection.
@@ -92,12 +100,79 @@
     this.nextSyntheticId = nextSyntheticId;
     this.legacySyntheticTypes = legacySyntheticTypes;
     this.nonLecacySyntheticItems = nonLecacySyntheticItems;
-    assert nonLecacySyntheticItems.keySet().stream()
-        .noneMatch(
-            t -> t.toDescriptorString().endsWith(getSyntheticDescriptorSuffix(nextSyntheticId)));
     assert Sets.intersection(nonLecacySyntheticItems.keySet(), legacySyntheticTypes).isEmpty();
   }
 
+  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();
+    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();
+    // 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;
+      }
+      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()) {
+      return;
+    }
+    CommittedItems commit =
+        new CommittedItems(
+            synthetics.nextSyntheticId,
+            appView.appInfo().app(),
+            synthetics.legacySyntheticTypes,
+            nonLegacySyntheticItems,
+            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 int getNextSyntheticId() {
@@ -110,14 +185,7 @@
 
   private static DexType hygienicType(
       DexItemFactory factory, int syntheticId, SynthesizingContext context) {
-    String contextDesc = context.type.toDescriptorString();
-    String prefix = contextDesc.substring(0, contextDesc.length() - 1);
-    String syntheticDesc = prefix + getSyntheticDescriptorSuffix(syntheticId);
-    return factory.createType(syntheticDesc);
-  }
-
-  private static String getSyntheticDescriptorSuffix(int syntheticId) {
-    return INTERNAL_SYNTHETIC_CLASS_SEPARATOR + syntheticId + ";";
+    return context.createHygienicType(syntheticId, factory);
   }
 
   // Predicates and accessors.
@@ -184,7 +252,7 @@
     SyntheticReference committedItemContext = nonLecacySyntheticItems.get(context.type);
     return committedItemContext != null
         ? committedItemContext.getContext()
-        : new SynthesizingContext(context.type, context.origin);
+        : SynthesizingContext.fromNonSyntheticInputClass(context);
   }
 
   // Addition and creation of synthetic items.
@@ -201,11 +269,11 @@
   public ProgramMethod createMethod(
       DexProgramClass context, DexItemFactory factory, Consumer<SyntheticMethodBuilder> fn) {
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
-    // The is to ensure a flat input-type -> synthetic-item mapping.
+    // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = getSynthesizingContext(context);
-    DexType type = hygienicType(factory, getNextSyntheticId(), outerContext);
-    DexProgramClass clazz =
-        new SyntheticClassBuilder(type, outerContext, factory).addMethod(fn).build();
+    DexType type = outerContext.createHygienicType(getNextSyntheticId(), factory);
+    SyntheticClassBuilder classBuilder = new SyntheticClassBuilder(type, outerContext, factory);
+    DexProgramClass clazz = classBuilder.addMethod(fn).build();
     ProgramMethod method = new ProgramMethod(clazz, clazz.methods().iterator().next());
     addPendingDefinition(new SyntheticMethodDefinition(outerContext, method));
     return method;
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 90e593c..7cc0c32 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -4,12 +4,14 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.graph.Code;
+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.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
+import java.util.List;
 
 public class SyntheticMethodBuilder {
 
@@ -22,6 +24,7 @@
   private DexProto proto = null;
   private SyntheticCodeGenerator codeGenerator = null;
   private MethodAccessFlags accessFlags = null;
+  private List<DexAnnotation> annotations = null;
 
   SyntheticMethodBuilder(SyntheticClassBuilder parent, String name) {
     this.parent = parent;
@@ -46,13 +49,30 @@
   DexEncodedMethod build() {
     boolean isCompilerSynthesized = true;
     DexMethod methodSignature = getMethodSignature();
-    return new DexEncodedMethod(
-        methodSignature,
-        getAccessFlags(),
-        getAnnotations(),
-        getParameterAnnotations(),
-        getCodeObject(methodSignature),
-        isCompilerSynthesized);
+    DexEncodedMethod method =
+        new DexEncodedMethod(
+            methodSignature,
+            getAccessFlags(),
+            getAnnotations(),
+            getParameterAnnotations(),
+            getCodeObject(methodSignature),
+            isCompilerSynthesized);
+    assert isValidSyntheticMethod(method);
+    return method;
+  }
+
+  /**
+   * Predicate for what is a "supported" synthetic method.
+   *
+   * <p>This method is used when identifying synthetic methods in the program input and should be as
+   * narrow as possible.
+   */
+  public static boolean isValidSyntheticMethod(DexEncodedMethod method) {
+    return method.isStatic()
+        && method.isNonAbstractNonNativeMethod()
+        && method.isPublic()
+        && method.annotations().isEmpty()
+        && method.getParameterAnnotations().isEmpty();
   }
 
   private DexMethod getMethodSignature() {
@@ -64,7 +84,9 @@
   }
 
   private DexAnnotationSet getAnnotations() {
-    return DexAnnotationSet.empty();
+    return annotations == null
+        ? DexAnnotationSet.empty()
+        : new DexAnnotationSet(annotations.toArray(DexAnnotation.EMPTY_ARRAY));
   }
 
   private ParameterAnnotationsList getParameterAnnotations() {
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 6fb07c5..90bcf51 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hasher;
@@ -60,7 +59,7 @@
   // 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::getContextType, DexType::slowCompareTo)
+    return Comparator.comparing(SyntheticMethodDefinition::getContext)
         .thenComparing(m -> m.method.getDefinition(), DexEncodedMethod::syntheticCompareTo)
         .compare(this, other);
   }
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 6070e49..86704ea 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
@@ -45,6 +45,6 @@
     DexMethod rewritten = lens.lookupMethod(method);
     return context == getContext() && rewritten == method
         ? this
-        : new SyntheticMethodReference(context, method);
+        : new SyntheticMethodReference(context, rewritten);
   }
 }
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 9b95337..408324e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticReference.java
@@ -6,7 +6,6 @@
 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.origin.Origin;
 import java.util.function.Function;
 
 /**
@@ -27,14 +26,6 @@
     return context;
   }
 
-  final DexType getContextType() {
-    return context.type;
-  }
-
-  final Origin getContextOrigin() {
-    return context.origin;
-  }
-
   abstract DexType getHolder();
 
   abstract SyntheticReference rewrite(NonIdentityGraphLens lens);
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 1a53b74..df274fe 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -91,8 +91,6 @@
 
   public enum DesugarState {
     OFF,
-    // This is for use when desugar has run before, and backports have still not been desugared.
-    ONLY_BACKPORT_STATICS,
     ON
   }
 
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
index 8f2e9b1..0f62c75 100644
--- a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8;
 
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
 
 public class GenerateMainDexListRunResult
@@ -16,6 +19,16 @@
     this.mainDexList = mainDexList;
   }
 
+  public List<ClassReference> getMainDexList() {
+    return ListUtils.map(
+        mainDexList,
+        entry -> {
+          assert entry.endsWith(".class");
+          String binaryName = entry.substring(0, entry.length() - ".class".length());
+          return Reference.classFromBinaryName(binaryName);
+        });
+  }
+
   @Override
   protected GenerateMainDexListRunResult self() {
     return this;
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 85705d2..86776a3 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -154,7 +154,7 @@
             features);
     switch (allowedDiagnosticMessages) {
       case ALL:
-        compileResult.assertDiagnosticThatMatches(new IsAnything<>());
+        compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
         break;
       case ERROR:
         compileResult.assertOnlyErrors();
diff --git a/src/test/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
index 4388754..1fe43fb 100644
--- a/src/test/java/com/android/tools/r8/TestBaseBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
@@ -6,14 +6,16 @@
 
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 public abstract class TestBaseBuilder<
         C extends BaseCommand,
@@ -69,14 +71,21 @@
     return self();
   }
 
+  public T addMainDexListClassReferences(ClassReference... classes) {
+    return addMainDexListClassReferences(Arrays.asList(classes));
+  }
+
+  public T addMainDexListClassReferences(Collection<ClassReference> classes) {
+    classes.forEach(c -> builder.addMainDexClasses(c.getTypeName()));
+    return self();
+  }
+
   public T addMainDexListClasses(Class<?>... classes) {
     return addMainDexListClasses(Arrays.asList(classes));
   }
 
   public T addMainDexListClasses(Collection<Class<?>> classes) {
-    builder.addMainDexClasses(
-        classes.stream().map(Class::getTypeName).collect(Collectors.toList()));
-    return self();
+    return addMainDexListClassReferences(ListUtils.map(classes, Reference::classFromClass));
   }
 
   public T addMainDexListFiles(Collection<Path> files) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
new file mode 100644
index 0000000..5907c15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
@@ -0,0 +1,130 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class ClassesWithFeatureSplitTest extends HorizontalClassMergingTestBase {
+  public ClassesWithFeatureSplitTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(Base.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(Feature1Class1.class, Feature1Class2.class, Feature1Main.class)
+            .addFeatureSplit(Feature2Class.class, Feature2Main.class)
+            .addKeepFeatureMainRule(Feature1Main.class)
+            .addKeepFeatureMainRule(Feature2Main.class)
+            .addOptionsModification(
+                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature1Main.class, compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("base", "feature 1 class 1", "feature 1 class 2");
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature2Main.class, compileResult.getFeature(1))
+        .assertSuccessWithOutputLines("base", "feature 2");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(Base.class), isPresent());
+    assertThat(inspector.clazz(Feature1Class1.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class2.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Class.class), not(isPresent()));
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Main.class), isPresent());
+    assertThat(inspector.clazz(Feature1Class1.class), isPresent());
+    assertThat(
+        inspector.clazz(Feature1Class2.class), notIf(isPresent(), enableHorizontalClassMerging));
+    assertThat(inspector.clazz(Feature2Main.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Class.class), not(isPresent()));
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Main.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class1.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature1Class2.class), not(isPresent()));
+    assertThat(inspector.clazz(Feature2Main.class), isPresent());
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+  }
+
+  @NeverClassInline
+  public static class Base {
+    public Base() {
+      System.out.println("base");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature1Class1 {
+    public Feature1Class1() {
+      System.out.println("feature 1 class 1");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature1Class2 {
+    public Feature1Class2() {
+      System.out.println("feature 1 class 2");
+    }
+  }
+
+  @NeverClassInline
+  public static class Feature2Class {
+    public Feature2Class() {
+      System.out.println("feature 2");
+    }
+  }
+
+  public static class Feature1Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Base();
+      new Feature1Class1();
+      new Feature1Class2();
+    }
+  }
+
+  public static class Feature2Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Base();
+      new Feature2Class();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
new file mode 100644
index 0000000..7dae8b6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
@@ -0,0 +1,130 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPackagePrivate;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPublic;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+
+public class ClassesWithOverlappingVisibilitiesTest extends HorizontalClassMergingTestBase {
+  public ClassesWithOverlappingVisibilitiesTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("foo A", "FOO B", "FOO C", "foo D", "FOO E")
+        .inspect(
+            codeInspector -> {
+              ClassSubject aClassSubject = codeInspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              MethodSubject methodSubject = aClassSubject.method("void", "foo");
+              assertThat(methodSubject, isPackagePrivate());
+
+              ClassSubject bClassSubject = codeInspector.clazz(B.class);
+              assertThat(bClassSubject, isPresent());
+              methodSubject = bClassSubject.method("void", "foo");
+              assertThat(methodSubject, isPackagePrivate());
+
+              assertThat(
+                  codeInspector.clazz(C.class), notIf(isPresent(), enableHorizontalClassMerging));
+
+              ClassSubject dClassSubject = codeInspector.clazz(D.class);
+              assertThat(dClassSubject, isPresent());
+              methodSubject = dClassSubject.method("void", "foo");
+              assertThat(methodSubject, isPublic());
+
+              ClassSubject eClassSubject = codeInspector.clazz(E.class);
+              assertThat(eClassSubject, notIf(isPresent(), enableHorizontalClassMerging));
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    @NeverInline
+    void foo() {
+      System.out.println("foo A");
+    }
+  }
+
+  @NeverClassInline
+  public static class B extends A {
+    public B() {
+      foo();
+    }
+
+    @Override
+    @NeverInline
+    void foo() {
+      System.out.println("FOO B");
+    }
+  }
+
+  // This class should be merged into B, as the method foo is package private both classes.
+  @NeverClassInline
+  public static class C extends A {
+    public C() {
+      foo();
+    }
+
+    @Override
+    @NeverInline
+    void foo() {
+      System.out.println("FOO C");
+    }
+  }
+
+  // This class can only be merged into E as D#foo is public, while A#foo is package private.
+  @NeverClassInline
+  public static class D {
+    @NeverInline
+    public void foo() {
+      System.out.println("foo D");
+    }
+  }
+
+  @NeverClassInline
+  public static class E {
+    public E() {
+      foo();
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("FOO E");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      A a = new A();
+      a.foo();
+      B b = new B();
+      C c = new C();
+      D d = new D();
+      d.foo();
+      E e = new E();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java
new file mode 100644
index 0000000..ee6dc18
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java
@@ -0,0 +1,76 @@
+// 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.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.ServiceLoader;
+import org.junit.Test;
+
+public class ServiceLoaderTest extends HorizontalClassMergingTestBase {
+  public ServiceLoaderTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    List<String> serviceImplementations = Lists.newArrayList();
+    serviceImplementations.add(B.class.getTypeName());
+    serviceImplementations.add(C.class.getTypeName());
+
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addDataEntryResources(
+            DataEntryResource.fromBytes(
+                StringUtils.lines(serviceImplementations).getBytes(),
+                "META-INF/services/" + A.class.getTypeName(),
+                Origin.unknown()))
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccess()
+        .inspect(
+            codeInspector -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(codeInspector.clazz(B.class), isPresent());
+              assertThat(codeInspector.clazz(C.class), isPresent());
+            });
+  }
+
+  public interface A {
+    String foo();
+  }
+
+  public static class B implements A {
+    @Override
+    public String foo() {
+      return "B";
+    }
+  }
+
+  public static class C implements A {
+    @Override
+    public String foo() {
+      return "C";
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      for (A a : ServiceLoader.load(A.class)) {
+        System.out.println(a.foo());
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithNonVisibleAnnotation.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithNonVisibleAnnotation.java
new file mode 100644
index 0000000..bb33801
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergingWithNonVisibleAnnotation.java
@@ -0,0 +1,88 @@
+// 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.classmerging.vertical;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.classmerging.vertical.testclasses.Outer;
+import com.android.tools.r8.classmerging.vertical.testclasses.Outer.Base;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.codeinspector.AnnotationSubject;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingWithNonVisibleAnnotation extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public VerticalClassMergingWithNonVisibleAnnotation(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(Outer.class)
+        .addProgramClasses(Sub.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Sub.class)
+        .addKeepClassRules(Outer.class.getPackage().getName() + ".Outer$Private* { *; }")
+        .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+        .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .run(parameters.getRuntime(), Sub.class)
+        .assertSuccessWithOutputLines("Base::foo()", "Sub::bar()")
+        .inspect(
+            codeInspector -> {
+              // Assert that base has been vertically merged into Sub.
+              assertThat(codeInspector.clazz(Base.class), not(isPresent()));
+              ClassSubject sub = codeInspector.clazz(Sub.class);
+              assertThat(sub, isPresent());
+              // Assert that the merged class has no annotations from Base
+              assertTrue(sub.getDexProgramClass().annotations().isEmpty());
+              // Assert that foo has the private annotation from the Base.foo
+              MethodSubject foo = sub.uniqueMethodWithName("foo");
+              assertThat(foo, isPresent());
+              AnnotationSubject privateMethodAnnotation =
+                  foo.annotation(
+                      Outer.class.getPackage().getName() + ".Outer$PrivateMethodAnnotation");
+              assertThat(privateMethodAnnotation, isPresent());
+            });
+  }
+
+  @NeverClassInline
+  public static class Sub extends Base {
+
+    @Override
+    @NeverInline
+    public void bar() {
+      System.out.println("Sub::bar()");
+    }
+
+    public static void main(String[] args) {
+      Sub sub = new Sub();
+      sub.foo();
+      sub.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/Outer.java b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/Outer.java
new file mode 100644
index 0000000..46ef8c8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/Outer.java
@@ -0,0 +1,34 @@
+// 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.classmerging.vertical.testclasses;
+
+import com.android.tools.r8.NeverInline;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class Outer {
+
+  @Retention(RetentionPolicy.RUNTIME)
+  private @interface PrivateClassAnnotation {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  private @interface PrivateMethodAnnotation {
+    Class<?> value();
+  }
+
+  private static class PrivateType {}
+
+  @PrivateClassAnnotation
+  public abstract static class Base {
+
+    @PrivateMethodAnnotation(PrivateType.class)
+    @NeverInline
+    public void foo() {
+      System.out.println("Base::foo()");
+    }
+
+    public abstract void bar();
+  }
+}
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 17e482f..c9377fe 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
@@ -68,7 +68,7 @@
       testForJvm()
           .addProgramFiles(jar)
           .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world!", "I::foo");
+          .assertSuccessWithOutputLines("Hello, world!", "I::foo", "J::bar", "42");
     } else {
       assert parameters.getRuntime().isDex();
       // Convert to DEX without desugaring.
@@ -77,7 +77,7 @@
           .setMinApi(parameters.getApiLevel())
           .disableDesugaring()
           .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world!", "I::foo");
+          .assertSuccessWithOutputLines("Hello, world!", "I::foo", "J::bar", "42");
     }
   }
 
@@ -87,18 +87,41 @@
     }
   }
 
+  interface J {
+    static void bar() {
+      System.out.println("J::bar");
+    }
+  }
+
   static class A implements I {}
 
   static class TestClass {
 
     public static void main(String[] args) {
+      lambda();
+      defaultInterfaceMethod();
+      staticInterfaceMethod();
+      backport();
+    }
+
+    public static void lambda() {
       Runnable runnable =
           () -> {
             System.out.println("Hello, world!");
           };
       runnable.run();
+    }
 
+    public static void defaultInterfaceMethod() {
       new A().foo();
     }
+
+    public static void staticInterfaceMethod() {
+      J.bar();
+    }
+
+    public static void backport() {
+      System.out.println(Long.hashCode(42));
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFileBackport.java b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFileBackport.java
index e07089f..2297f3f 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFileBackport.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFileBackport.java
@@ -64,7 +64,7 @@
     MethodSubject methodSubject = inspector.clazz(TestClass.class).uniqueMethodWithName("main");
     if (methodSubject.getProgramMethod().getDefinition().getCode().isCfCode()) {
       CfCode code = methodSubject.getProgramMethod().getDefinition().getCode().asCfCode();
-      assertTrue(code.instructions.stream().noneMatch(this::isCfLAdd));
+      assertTrue(code.getInstructions().stream().noneMatch(this::isCfLAdd));
     } else {
       DexCode code = methodSubject.getProgramMethod().getDefinition().getCode().asDexCode();
       assertTrue(Arrays.stream(code.instructions).noneMatch(this::isDexAddLong));
@@ -76,7 +76,7 @@
     MethodSubject methodSubject = inspector.clazz(TestClass.class).uniqueMethodWithName("main");
     if (methodSubject.getProgramMethod().getDefinition().getCode().isCfCode()) {
       CfCode code = methodSubject.getProgramMethod().getDefinition().getCode().asCfCode();
-      assertTrue(code.instructions.stream().anyMatch(this::isCfLAdd));
+      assertTrue(code.getInstructions().stream().anyMatch(this::isCfLAdd));
     } else {
       DexCode code = methodSubject.getProgramMethod().getDefinition().getCode().asDexCode();
       assertTrue(Arrays.stream(code.instructions).anyMatch(this::isDexAddLong));
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 2913fe5..31575df 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
@@ -4,7 +4,6 @@
 package com.android.tools.r8.desugar.backports;
 
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.hasItems;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -13,14 +12,20 @@
 import com.android.tools.r8.TestParameters;
 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.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 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;
@@ -68,26 +73,86 @@
 
   @Test
   public void testD8() throws Exception {
-    List<String> run1 = getClassesAfterD8CompileAndRun();
-    List<String> run2 = getClassesAfterD8CompileAndRun();
-    assertEquals("Non deterministic synthesis", run1, run2);
-  }
-
-  private List<String> getClassesAfterD8CompileAndRun() throws Exception {
-    return testForD8(parameters.getBackend())
+    testForD8(parameters.getBackend())
         .addProgramClasses(CLASSES)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput(EXPECTED)
         .inspect(this::checkNoInternalSyntheticNames)
-        .inspect(this::checkExpectedOutput)
-        .inspector()
-        .allClasses()
-        .stream()
-        .filter(c -> !CLASS_TYPE_NAMES.contains(c.getFinalName()))
-        .flatMap(c -> c.allMethods().stream().map(m -> m.asMethodReference().toString()))
-        .sorted()
-        .collect(Collectors.toList());
+        .inspect(this::checkExpectedSynthetics);
+  }
+
+  @Test
+  public void testD8Merging() throws Exception {
+    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()
+            .addProgramClasses(User1.class)
+            .addClasspathClasses(CLASSES)
+            .setMinApi(parameters.getApiLevel())
+            .setIntermediate(intermediate)
+            .compile()
+            .writeToZip();
+
+    // Compile part 2 of the input (maybe intermediate)
+    Path out2 =
+        testForD8()
+            .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()
+        .addProgramClasses(MiniAssert.class, TestClass.class)
+        .addProgramFiles(out1, out2)
+        .setMinApi(parameters.getApiLevel())
+        .setIntermediate(true)
+        .compile()
+        .writeToZip(out3)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoInternalSyntheticNames)
+        .inspect(inspector -> assertEquals(syntheticsInParts, getSyntheticMethods(inspector)));
+
+    // Finally do a non-intermediate merge.
+    testForD8()
+        .addProgramFiles(out3)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkNoInternalSyntheticNames)
+        .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));
+              }
+            });
   }
 
   private void checkNoInternalSyntheticNames(CodeInspector inspector) {
@@ -99,24 +164,28 @@
         });
   }
 
-  private void checkExpectedOutput(CodeInspector inspector) {
-    // TODO(b/158159959): Once synthetic methods can be grouped in classes this should become 1.
-    int expectedSynthesizedClasses = 3;
-    // Total number of synthetic methods should be 3 ({Boolean,Character,Long}.compare).
-    int expectedSynthesizedMethods = 3;
-    // Desugaring should add exactly one class with one desugared method.
-    assertEquals(expectedSynthesizedClasses, inspector.allClasses().size() - CLASSES.size());
-    assertThat(
-        inspector.allClasses().stream()
-            .map(ClassSubject::getOriginalName)
-            .collect(Collectors.toList()),
-        hasItems(CLASS_TYPE_NAMES.toArray()));
-    List<FoundMethodSubject> methods =
-        inspector.allClasses().stream()
-            .filter(clazz -> !CLASS_TYPE_NAMES.contains(clazz.getOriginalName()))
-            .flatMap(clazz -> clazz.allMethods().stream())
-            .collect(Collectors.toList());
-    assertEquals(expectedSynthesizedMethods, methods.size());
+  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().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.syntheticMethod(
+                User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
+            SyntheticItemsTestUtils.syntheticMethod(
+                User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)),
+            SyntheticItemsTestUtils.syntheticMethod(
+                User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
+    assertEquals(expectedSynthetics, getSyntheticMethods(inspector));
   }
 
   static class User1 {
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 3be0a2c..424a48f 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
@@ -5,7 +5,6 @@
 
 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.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -15,19 +14,33 @@
 import com.android.tools.r8.ByteDataView;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.GenerateMainDexListRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.desugar.backports.AbstractBackportTest.MiniAssert;
 import com.android.tools.r8.origin.Origin;
+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.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;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+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;
@@ -40,6 +53,9 @@
   static final List<Class<?>> CLASSES =
       ImmutableList.of(MiniAssert.class, TestClass.class, User1.class, User2.class);
 
+  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 =
@@ -78,15 +94,50 @@
         .assertSuccessWithOutput(EXPECTED);
   }
 
+  private GenerateMainDexListRunResult traceMainDex(
+      Collection<Class<?>> classes, Collection<Path> files) throws Exception {
+    return testForMainDexListGenerator()
+        .addProgramClasses(classes)
+        .addProgramFiles(files)
+        .addLibraryFiles(ToolHelper.getFirstSupportedAndroidJar(parameters.getApiLevel()))
+        .addMainDexRules(keepMainProguardConfiguration(TestClass.class))
+        .run();
+  }
+
+  @Test
+  public void testMainDexTracingCf() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    GenerateMainDexListRunResult mainDexListFromCf = traceMainDex(CLASSES, Collections.emptyList());
+    assertEquals(
+        ListUtils.map(MAIN_DEX_LIST_CLASSES, Reference::classFromClass),
+        mainDexListFromCf.getMainDexList());
+  }
+
+  @Test
+  public void testMainDexTracingDex() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    Path out =
+        testForD8()
+            .addProgramClasses(CLASSES)
+            // Setting intermediate will annotate synthetics, which should not cause types in those
+            // to become main-dex included.
+            .setIntermediate(true)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+    GenerateMainDexListRunResult mainDexListFromDex =
+        traceMainDex(Collections.emptyList(), Collections.singleton(out));
+    assertEquals(
+        Streams.concat(
+                MAIN_DEX_LIST_CLASSES.stream().map(Reference::classFromClass),
+                getMainDexExpectedSynthetics().stream().map(MethodReference::getHolderClass))
+            .collect(Collectors.toSet()),
+        ImmutableSet.copyOf(mainDexListFromDex.getMainDexList()));
+  }
+
   @Test
   public void testD8() throws Exception {
     assumeTrue(parameters.isDexRuntime());
-    Set<String> mainDex1 = runD8();
-    Set<String> mainDex2 = runD8();
-    assertEquals("Expected deterministic main-dex lists", mainDex1, mainDex2);
-  }
-
-  private Set<String> runD8() throws Exception {
     MainDexConsumer mainDexConsumer = new MainDexConsumer();
     testForD8(parameters.getBackend())
         .addProgramClasses(CLASSES)
@@ -94,39 +145,89 @@
         .addMainDexListClasses(MiniAssert.class, TestClass.class, User2.class)
         .setProgramConsumer(mainDexConsumer)
         .compile()
-        .inspect(
-            inspector -> {
-              // Note: This will change if we group methods in classes, in which case we should
-              // preferable not put non-main-dex referenced methods in the main-dex group.
-              // User1 has two synthetics, one shared with User2 and one for self.
-              assertEquals(
-                  2,
-                  inspector.allClasses().stream()
-                      .filter(c -> c.getFinalName().startsWith(SyntheticUnderUser1))
-                      .count());
-              // User2 has one synthetic as the shared call is placed in User1.
-              assertEquals(
-                  1,
-                  inspector.allClasses().stream()
-                      .filter(c -> c.getFinalName().startsWith(SyntheticUnderUser2))
-                      .count());
-            })
+        .inspect(this::checkExpectedSynthetics)
         .run(parameters.getRuntime(), TestClass.class, getRunArgs())
         .assertSuccessWithOutput(EXPECTED);
     checkMainDex(mainDexConsumer);
-    return mainDexConsumer.mainDexDescriptors;
+  }
+
+  // TODO(b/168584485): This test should be removed once support is dropped.
+  @Test
+  public void testD8MergingWithTraceCf() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    Path out1 =
+        testForD8()
+            .addProgramClasses(User1.class)
+            .addClasspathClasses(CLASSES)
+            .setIntermediate(true)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    Path out2 =
+        testForD8()
+            .addProgramClasses(User2.class)
+            .addClasspathClasses(CLASSES)
+            .setIntermediate(true)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    MainDexConsumer mainDexConsumer = new MainDexConsumer();
+    testForD8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, MiniAssert.class)
+        .addProgramFiles(out1, out2)
+        .setMinApi(parameters.getApiLevel())
+        .addMainDexListClassReferences(
+            traceMainDex(CLASSES, Collections.emptyList()).getMainDexList())
+        .setProgramConsumer(mainDexConsumer)
+        .compile()
+        .inspect(this::checkExpectedSynthetics)
+        .run(parameters.getRuntime(), TestClass.class, getRunArgs())
+        .assertSuccessWithOutput(EXPECTED);
+    checkMainDex(mainDexConsumer);
+  }
+
+  @Test
+  public void testD8MergingWithTraceDex() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    Path out1 =
+        testForD8()
+            .addProgramClasses(User1.class)
+            .addClasspathClasses(CLASSES)
+            .setIntermediate(true)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    Path out2 =
+        testForD8()
+            .addProgramClasses(User2.class)
+            .addClasspathClasses(CLASSES)
+            .setIntermediate(true)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    MainDexConsumer mainDexConsumer = new MainDexConsumer();
+    List<Class<?>> classes = ImmutableList.of(TestClass.class, MiniAssert.class);
+    List<Path> files = ImmutableList.of(out1, out2);
+    GenerateMainDexListRunResult traceResult = traceMainDex(classes, files);
+    testForD8(parameters.getBackend())
+        .addProgramClasses(classes)
+        .addProgramFiles(files)
+        .setMinApi(parameters.getApiLevel())
+        .addMainDexListClassReferences(traceResult.getMainDexList())
+        .setProgramConsumer(mainDexConsumer)
+        .compile()
+        .inspect(this::checkExpectedSynthetics)
+        .run(parameters.getRuntime(), TestClass.class, getRunArgs())
+        .assertSuccessWithOutput(EXPECTED);
+    checkMainDex(mainDexConsumer);
   }
 
   @Test
   public void testR8() throws Exception {
-    Set<String> mainDex1 = runR8();
-    if (parameters.isDexRuntime()) {
-      Set<String> mainDex2 = runR8();
-      assertEquals(mainDex1, mainDex2);
-    }
-  }
-
-  private Set<String> runR8() throws Exception {
     MainDexConsumer mainDexConsumer = parameters.isDexRuntime() ? new MainDexConsumer() : null;
     testForR8(parameters.getBackend())
         .debug() // Use debug mode to force a minimal main dex.
@@ -135,18 +236,17 @@
         .addProgramClasses(CLASSES)
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(MiniAssert.class)
-        .addMainDexClassRules(MiniAssert.class, TestClass.class)
         .addKeepMethodRules(
             Reference.methodFromMethod(User1.class.getMethod("testBooleanCompare")),
             Reference.methodFromMethod(User1.class.getMethod("testCharacterCompare")))
+        .addMainDexRules(keepMainProguardConfiguration(TestClass.class))
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class, getRunArgs())
-        .assertSuccessWithOutput(EXPECTED);
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkExpectedSynthetics);
     if (mainDexConsumer != null) {
       checkMainDex(mainDexConsumer);
-      return mainDexConsumer.mainDexDescriptors;
     }
-    return null;
   }
 
   private void checkMainDex(MainDexConsumer mainDexConsumer) throws Exception {
@@ -160,22 +260,49 @@
     assertThat(mainDexInspector.clazz(MiniAssert.class), isPresent());
     assertThat(mainDexInspector.clazz(TestClass.class), isPresent());
     assertThat(mainDexInspector.clazz(User2.class), isPresent());
+    assertEquals(getMainDexExpectedSynthetics(), getSyntheticMethods(mainDexInspector));
+  }
 
-    // At least one synthetic class placed under User2 must be included in the main-dex file.
-    assertEquals(
-        1,
-        mainDexInspector.allClasses().stream()
-            .filter(c -> c.getFinalName().startsWith(SyntheticUnderUser2))
-            .count());
+  private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
+    Set<ClassReference> nonSyntheticCLasses =
+        CLASSES.stream().map(Reference::classFromClass).collect(Collectors.toSet());
+    Set<MethodReference> methods = new HashSet<>();
+    inspector.allClasses().stream()
+        .filter(c -> !nonSyntheticCLasses.contains(c.getFinalReference()))
+        .forEach(c -> c.allMethods().forEach(m -> methods.add(m.asMethodReference())));
+    return methods;
+  }
 
-    // Minimal main dex should only include one of the User1 synthetics.
-    assertThat(mainDexInspector.clazz(User1.class), not(isPresent()));
-    assertEquals(
-        1,
-        mainDexInspector.allClasses().stream()
-            .filter(c -> c.getFinalName().startsWith(SyntheticUnderUser1))
-            .count());
-    assertThat(mainDexInspector.clazz(SyntheticUnderUser1), not(isPresent()));
+  private void checkExpectedSynthetics(CodeInspector inspector) throws Exception {
+    if (parameters.getApiLevel() == null) {
+      assertEquals(Collections.emptySet(), getSyntheticMethods(inspector));
+    } else {
+      assertEquals(
+          Sets.union(getMainDexExpectedSynthetics(), getNonMainDexExpectedSynthetics()),
+          getSyntheticMethods(inspector));
+    }
+  }
+
+  // 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.
+
+  private ImmutableSet<MethodReference> getNonMainDexExpectedSynthetics()
+      throws NoSuchMethodException {
+    return ImmutableSet.of(
+        SyntheticItemsTestUtils.syntheticMethod(
+            User1.class, 1, Boolean.class.getMethod("compare", boolean.class, boolean.class)));
+  }
+
+  private ImmutableSet<MethodReference> getMainDexExpectedSynthetics()
+      throws NoSuchMethodException {
+    return ImmutableSet.of(
+        SyntheticItemsTestUtils.syntheticMethod(
+            User1.class, 0, Character.class.getMethod("compare", char.class, char.class)),
+        SyntheticItemsTestUtils.syntheticMethod(
+            User2.class, 0, Integer.class.getMethod("compare", int.class, int.class)));
   }
 
   static class User1 {
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/DesugarStaticBackportsOnly.java b/src/test/java/com/android/tools/r8/desugar/backports/DesugarStaticBackportsOnly.java
deleted file mode 100644
index 77e088c..0000000
--- a/src/test/java/com/android/tools/r8/desugar/backports/DesugarStaticBackportsOnly.java
+++ /dev/null
@@ -1,137 +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.desugar.backports;
-
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.StringContains.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.D8TestBuilder;
-import com.android.tools.r8.Diagnostic;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.synthesis.SyntheticItems;
-import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.InternalOptions.DesugarState;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.DexInstructionSubject;
-import java.util.Arrays;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class DesugarStaticBackportsOnly extends TestBase {
-
-  private final TestParameters parameters;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
-  }
-
-  public DesugarStaticBackportsOnly(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  private void checkLongHashCodeDesugared(CodeInspector inspector) {
-    ClassSubject classSubject = inspector.clazz(TestClass.class);
-    assertThat(classSubject, isPresent());
-    assertEquals(
-        parameters.getApiLevel().isLessThan(AndroidApiLevel.N),
-        classSubject
-            .uniqueMethodWithName("main")
-            .streamInstructions()
-            .anyMatch(
-                instructionSubject ->
-                    instructionSubject.isInvokeStatic()
-                        && instructionSubject
-                            .toString()
-                            .contains(SyntheticItems.EXTERNAL_SYNTHETIC_CLASS_SEPARATOR)));
-    assertEquals(
-        parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N),
-        classSubject
-            .uniqueMethodWithName("main")
-            .streamInstructions()
-            .anyMatch(
-                instructionSubject ->
-                    instructionSubject.isInvokeStatic()
-                        && instructionSubject.toString().contains("java/lang/Long")));
-  }
-
-  @Test
-  public void testBackportDesugared() throws Exception {
-    String expectedOutput = StringUtils.lines("1234");
-    testForD8()
-        .addProgramClasses(TestClass.class)
-        .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(
-            options -> options.desugarState = DesugarState.ONLY_BACKPORT_STATICS)
-        .compile()
-        .inspect(this::checkLongHashCodeDesugared)
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(expectedOutput);
-  }
-
-  private void checkLambdaNotDesugared(CodeInspector inspector) {
-    ClassSubject classSubject = inspector.clazz(TestClassLambda.class);
-    assertThat(classSubject, isPresent());
-    assertTrue(
-        classSubject
-            .uniqueMethodWithName("main")
-            .streamInstructions()
-            .anyMatch(
-                instructionSubject ->
-                    ((DexInstructionSubject) instructionSubject).isInvokeCustom()));
-  }
-
-  @Test
-  public void testLambdaNotDesugared() throws Exception {
-    D8TestBuilder builder =
-        testForD8()
-            .addProgramClasses(TestClassLambda.class)
-            .setMinApi(parameters.getApiLevel())
-            .addOptionsModification(
-                options -> options.desugarState = DesugarState.ONLY_BACKPORT_STATICS);
-    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O)) {
-      builder.compile().inspect(this::checkLambdaNotDesugared);
-    } else {
-      try {
-        builder.compileWithExpectedDiagnostics(
-            diagnostics -> {
-              diagnostics.assertOnlyErrors();
-              diagnostics.assertErrorsCount(1);
-              Diagnostic diagnostic = diagnostics.getErrors().get(0);
-              assertThat(
-                  diagnostic.getDiagnosticMessage(),
-                  containsString("Invoke-customs are only supported starting with Android O"));
-            });
-      } catch (CompilationFailedException e) {
-        // Expected compilation failed.
-        return;
-      }
-      fail("Expected test to fail with CompilationFailedException");
-    }
-  }
-
-  static class TestClass {
-    public static void main(String[] args) {
-      System.out.println(Long.hashCode(1234));
-    }
-  }
-
-  static class TestClassLambda {
-    public static void main(String[] args) {
-      Arrays.asList(args).forEach(s -> System.out.println(s));
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingTest.java
index 8aa2d5e..7a0c8bb 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingTest.java
@@ -17,8 +17,6 @@
 @RunWith(Parameterized.class)
 public class PhiEnumUnboxingTest extends EnumUnboxingTestBase {
 
-  private static final Class<?> ENUM_CLASS = MyEnum.class;
-
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
   private final EnumKeepRules enumKeepRules;
@@ -37,11 +35,10 @@
 
   @Test
   public void testEnumUnboxing() throws Exception {
-    Class<?> classToTest = Phi.class;
     R8TestRunResult run =
         testForR8(parameters.getBackend())
-            .addProgramClasses(classToTest, ENUM_CLASS)
-            .addKeepMainRule(classToTest)
+            .addProgramClasses(Phi.class, MyEnum.class)
+            .addKeepMainRule(Phi.class)
             .addKeepRules(enumKeepRules.getKeepRules())
             .enableInliningAnnotations()
             .enableNeverClassInliningAnnotations()
@@ -50,8 +47,8 @@
             .setMinApi(parameters.getApiLevel())
             .compile()
             .inspectDiagnosticMessages(
-                m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
-            .run(parameters.getRuntime(), classToTest)
+                m -> assertEnumIsUnboxed(MyEnum.class, Phi.class.getSimpleName(), m))
+            .run(parameters.getRuntime(), Phi.class)
             .assertSuccess();
     assertLines2By2Correct(run.getStdOut());
   }
@@ -66,23 +63,54 @@
   static class Phi {
 
     public static void main(String[] args) {
+      nonNullTest();
+      nullTest();
+    }
+
+    private static void nonNullTest() {
       System.out.println(switchOn(1).ordinal());
       System.out.println(1);
       System.out.println(switchOn(2).ordinal());
       System.out.println(2);
     }
 
-    // Avoid removing the switch entirely.
+    private static void nullTest() {
+      System.out.println(switchOnWithNull(1).ordinal());
+      System.out.println(1);
+      System.out.println(switchOnWithNull(2) == null);
+      System.out.println(true);
+    }
+
     @NeverInline
     static MyEnum switchOn(int i) {
+      MyEnum returnValue;
       switch (i) {
         case 0:
-          return MyEnum.A;
+          returnValue = MyEnum.A;
+          break;
         case 1:
-          return MyEnum.B;
+          returnValue = MyEnum.B;
+          break;
         default:
-          return MyEnum.C;
+          returnValue = MyEnum.C;
       }
+      return returnValue;
+    }
+
+    @NeverInline
+    static MyEnum switchOnWithNull(int i) {
+      MyEnum returnValue;
+      switch (i) {
+        case 0:
+          returnValue = MyEnum.A;
+          break;
+        case 1:
+          returnValue = MyEnum.B;
+          break;
+        default:
+          returnValue = null;
+      }
+      return returnValue;
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java
index dc4ed0e..87de375 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java
@@ -104,7 +104,7 @@
       TryHandler handler = dexCode.handlers[0];
       assertEquals(1, handler.pairs.length);
 
-      DexType guard = handler.pairs[0].type;
+      DexType guard = handler.pairs[0].getType();
       assertEquals("java.lang.Exception", guard.toSourceString());
     } else {
       assert code.isCfCode();
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 d1ecf46..6ef399e 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.maindexlist;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.withNativeFileSeparators;
@@ -20,6 +21,7 @@
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompilerBuilder;
+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;
@@ -46,7 +48,11 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class MainDexTracingTest extends TestBase {
 
   private static final String EXAMPLE_BUILD_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
@@ -54,6 +60,31 @@
   private static final String EXAMPLE_SRC_DIR = ToolHelper.EXAMPLES_DIR;
   private static final String EXAMPLE_O_SRC_DIR = ToolHelper.EXAMPLES_ANDROID_O_DIR;
 
+  @Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), Backend.values());
+  }
+
+  private final Backend backend;
+
+  public MainDexTracingTest(TestParameters parameters, Backend backend) {
+    parameters.assertNoneRuntime();
+    this.backend = backend;
+  }
+
+  private Path getInputJar(Path cfJar) throws Exception {
+    if (backend == Backend.CF) {
+      return cfJar;
+    }
+    return testForD8()
+        .setIntermediate(true)
+        .addProgramFiles(cfJar)
+        .setMinApi(AndroidApiLevel.K)
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.K))
+        .compile()
+        .writeToZip();
+  }
+
   @Test
   public void traceMainDexList001_whyareyoukeeping() throws Throwable {
     PrintStream stdout = System.out;
@@ -294,7 +325,7 @@
       throws Throwable {
     Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
 
-    Path inputJar = Paths.get(buildDir, packageName + JAR_EXTENSION);
+    Path inputJar = getInputJar(Paths.get(buildDir, packageName + JAR_EXTENSION));
     // Build main-dex list using GenerateMainDexList and test the output from run.
     GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder();
     GenerateMainDexListCommand mdlCommand = mdlCommandBuilder
@@ -335,14 +366,22 @@
         .addKeepRules("-keepattributes *Annotation*")
         .addMainDexRuleFiles(mainDexRules)
         .apply(configuration)
-        .allowDiagnosticWarningMessages()
         .assumeAllMethodsMayHaveSideEffects()
         .setMinApi(minSdk)
         .noMinification()
         .noTreeShaking()
         .setMainDexListConsumer(ToolHelper.consumeString(r8MainDexListOutput::set))
-        .compile()
-        .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
+        .allowDiagnosticMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics.assertNoInfos().assertNoErrors();
+              if (backend == Backend.CF) {
+                diagnostics.assertWarningsMatch(
+                    diagnosticMessage(equalTo("Resource 'META-INF/MANIFEST.MF' already exists.")));
+              } else {
+                diagnostics.assertNoWarnings();
+              }
+            })
         .writeToZip(out);
 
     List<String> r8MainDexList =
@@ -371,9 +410,16 @@
         if (mainDexGeneratorMainDexList.size() <= i - nonLambdaOffset) {
           fail("Main dex list generator is missing '" + reference + "'");
         }
-        checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset));
-        checkSameMainDexEntry(
-            reference, mainDexGeneratorMainDexListFromConsumer.get(i - nonLambdaOffset));
+        String fromList = mainDexGeneratorMainDexList.get(i - nonLambdaOffset);
+        String fromConsumer = mainDexGeneratorMainDexListFromConsumer.get(i - nonLambdaOffset);
+        if (isLambda(fromList)) {
+          assertEquals(Backend.DEX, backend);
+          assertEquals(fromList, fromConsumer);
+          nonLambdaOffset--;
+        } else {
+          checkSameMainDexEntry(reference, fromList);
+          checkSameMainDexEntry(reference, fromConsumer);
+        }
       } else {
         nonLambdaOffset++;
       }
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithCatchHandlerTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithCatchHandlerTest.java
new file mode 100644
index 0000000..3afa4c2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithCatchHandlerTest.java
@@ -0,0 +1,66 @@
+// 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.repackage;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithCatchHandlerTest extends RepackageTestBase {
+
+  public RepackageWithCatchHandlerTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .apply(this::configureRepackaging)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    assertThat(inspector.clazz(MyException.class), isPresent());
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) {
+      try {
+        raise();
+      } catch (MyException e) {
+        e.greet();
+      }
+    }
+
+    @NeverInline
+    static void raise() throws MyException {
+      throw new MyException();
+    }
+  }
+
+  public static class MyException extends Exception {
+
+    @NeverInline
+    public static void greet() {
+      System.out.println("Hello world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java
new file mode 100644
index 0000000..8a36090
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithDexItemBasedConstStringTest.java
@@ -0,0 +1,75 @@
+// 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.repackage;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject.JumboStringMode;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithDexItemBasedConstStringTest extends RepackageTestBase {
+
+  public RepackageWithDexItemBasedConstStringTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .apply(this::configureRepackaging)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    MethodSubject mainMethodSubject = testClassSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertTrue(
+        mainMethodSubject
+            .streamInstructions()
+            .anyMatch(x -> x.isConstString(aClassSubject.getFinalName(), JumboStringMode.ALLOW)));
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) throws Exception {
+      Class.forName("com.android.tools.r8.repackage.RepackageWithDexItemBasedConstStringTest$A")
+          .getConstructor()
+          .newInstance();
+    }
+  }
+
+  public static class A {
+
+    static {
+      System.out.print("Hello");
+    }
+
+    public A() {
+      System.out.println(" world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/InliningClassVersionTest.java b/src/test/java/com/android/tools/r8/shaking/examples/InliningClassVersionTest.java
index e47222b..5615c6d 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/InliningClassVersionTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/InliningClassVersionTest.java
@@ -7,24 +7,37 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.ArchiveClassFileProvider;
-import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.NeverPropagateValue;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.io.ByteStreams;
 import java.nio.file.Path;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.Opcodes;
 
+@RunWith(Parameterized.class)
 public class InliningClassVersionTest extends TestBase {
 
+  private final TestParameters parameters;
+  private static final String EXPECTED = "Hello from Inlinee!";
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntimes().build();
+  }
+
+  public InliningClassVersionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   private final int OLD_VERSION = Opcodes.V1_6;
   private final String BASE_DESCRIPTOR = DescriptorUtils.javaTypeToDescriptor(Base.class.getName());
 
@@ -43,59 +56,44 @@
     }
   }
 
-  private static class DowngradeVisitor extends ClassVisitor {
-
-    private final int version;
-
-    DowngradeVisitor(ClassVisitor cv, int version) {
-      super(InternalOptions.ASM_VERSION, cv);
-      this.version = version;
-    }
-
-    @Override
-    public void visit(
-        int version,
-        int access,
-        String name,
-        String signature,
-        String superName,
-        String[] interfaces) {
-      assert version > this.version
-          : "Going from " + version + " to " + this.version + " is not a downgrade";
-      super.visit(this.version, access, name, signature, superName, interfaces);
-    }
-  }
-
-  private static byte[] downgradeClass(byte[] classBytes, int version) {
-    ClassWriter writer = new ClassWriter(0);
-    new ClassReader(classBytes).accept(new DowngradeVisitor(writer, version), 0);
-    return writer.toByteArray();
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Inlinee.class)
+        .addProgramClassFileData(downgradeBaseClass())
+        .run(parameters.getRuntime(), Base.class)
+        .assertSuccessWithOutputLines(EXPECTED);
   }
 
   @Test
   public void test() throws Exception {
-    Path inputJar = writeInput();
-    assertEquals(OLD_VERSION, getBaseClassVersion(inputJar));
-    ProcessResult runInput = run(inputJar);
-    assertEquals(0, runInput.exitCode);
-    Path outputJar =
-        testForR8(Backend.CF)
-            .addProgramFiles(inputJar)
-            .addKeepMainRule(Base.class)
-            .enableMemberValuePropagationAnnotations()
-            .compile()
-            .writeToZip();
-    ProcessResult runOutput = run(outputJar);
-    assertEquals(runInput.toString(), runOutput.toString());
+    Path outputJar = temp.newFile("output.jar").toPath();
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Inlinee.class)
+        .addProgramClassFileData(downgradeBaseClass())
+        .addKeepMainRule(Base.class)
+        .enableMemberValuePropagationAnnotations()
+        .compile()
+        .writeToZip(outputJar)
+        .run(parameters.getRuntime(), Base.class)
+        .assertSuccessWithOutputLines(EXPECTED);
     assertNotEquals(
         "Inliner did not upgrade classfile version", OLD_VERSION, getBaseClassVersion(outputJar));
   }
 
-  private int getBaseClassVersion(Path jar) throws Exception {
-    return getClassVersion(jar, BASE_DESCRIPTOR);
+  private byte[] downgradeBaseClass() throws Exception {
+    byte[] transform = transformer(Base.class).setVersion(OLD_VERSION).transform();
+    assertEquals(OLD_VERSION, getClassVersion(transform));
+    return transform;
   }
 
-  private int getClassVersion(Path jar, String descriptor) throws Exception {
+  private int getBaseClassVersion(Path jar) throws Exception {
+    return getClassVersion(
+        ByteStreams.toByteArray(
+            new ArchiveClassFileProvider(jar).getProgramResource(BASE_DESCRIPTOR).getByteStream()));
+  }
+
+  private int getClassVersion(byte[] classFileBytes) {
 
     class ClassVersionReader extends ClassVisitor {
       private int version = -1;
@@ -117,32 +115,9 @@
         this.version = version;
       }
     }
-
-    byte[] bytes =
-        ByteStreams.toByteArray(
-            new ArchiveClassFileProvider(jar).getProgramResource(descriptor).getByteStream());
     ClassVersionReader reader = new ClassVersionReader();
-    new ClassReader(bytes).accept(reader, 0);
+    new ClassReader(classFileBytes).accept(reader, 0);
     assert reader.version != -1;
     return reader.version;
   }
-
-  private Path writeInput() throws Exception {
-    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
-    ClassFileConsumer consumer = new ClassFileConsumer.ArchiveConsumer(inputJar);
-    consumer.accept(
-        ByteDataView.of(downgradeClass(ToolHelper.getClassAsBytes(Base.class), OLD_VERSION)),
-        BASE_DESCRIPTOR,
-        null);
-    consumer.accept(
-        ByteDataView.of(ToolHelper.getClassAsBytes(Inlinee.class)),
-        DescriptorUtils.javaTypeToDescriptor(Inlinee.class.getName()),
-        null);
-    consumer.finished(null);
-    return inputJar;
-  }
-
-  private ProcessResult run(Path jar) throws Exception {
-    return ToolHelper.runJava(jar, Base.class.getName());
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java
new file mode 100644
index 0000000..9e78bac
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/SyntheticItemsTestUtils.java
@@ -0,0 +1,27 @@
+// 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 fe4d4d9..978db99 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
@@ -111,6 +111,11 @@
   }
 
   @Override
+  public ClassReference getFinalReference() {
+    return null;
+  }
+
+  @Override
   public String getFinalName() {
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfTryCatchSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfTryCatchSubject.java
index d6bac64..5853fdd 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfTryCatchSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfTryCatchSubject.java
@@ -31,7 +31,7 @@
     int index = 0;
     int startIndex = -1;
     int endIndex = -1;
-    for (CfInstruction instruction : cfCode.instructions) {
+    for (CfInstruction instruction : cfCode.getInstructions()) {
       if (startIndex < 0 && instruction.equals(tryCatch.start)) {
         startIndex = index;
       }
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 96b0d0c..6c7368b 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
@@ -183,6 +183,8 @@
 
   public abstract String getOriginalBinaryName();
 
+  public abstract ClassReference getFinalReference();
+
   public abstract String getFinalName();
 
   public abstract String getFinalDescriptor();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexTryCatchSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexTryCatchSubject.java
index 4624c33..c1fa59a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexTryCatchSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexTryCatchSubject.java
@@ -38,8 +38,8 @@
   @Override
   public boolean isCatching(String exceptionType) {
     for (TypeAddrPair pair : tryHandler.pairs) {
-      if (pair.type.toString().equals(exceptionType)
-        || pair.type.toDescriptorString().equals(exceptionType)) {
+      if (pair.getType().toString().equals(exceptionType)
+          || pair.getType().toDescriptorString().equals(exceptionType)) {
         return true;
       }
     }
@@ -54,7 +54,7 @@
   @Override
   public Stream<TypeSubject> streamGuards() {
     return Arrays.stream(tryHandler.pairs)
-        .map(pair -> pair.type)
+        .map(pair -> pair.getType())
         .map(type -> new TypeSubject(inspector, type));
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
index d4cd896..e63a9fd 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FieldSubject.java
@@ -20,6 +20,7 @@
 
   public abstract DexValue getStaticValue();
 
+  @Override
   public abstract boolean isRenamed();
 
   public abstract String getOriginalSignatureAttribute();
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 dbdff3d..f42bc3e 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
@@ -28,6 +28,7 @@
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
@@ -294,6 +295,11 @@
   }
 
   @Override
+  public ClassReference getFinalReference() {
+    return Reference.classFromDescriptor(getFinalDescriptor());
+  }
+
+  @Override
   public String getFinalName() {
     return DescriptorUtils.descriptorToJavaType(getFinalDescriptor());
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 04f007b..4f4c340 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -299,6 +299,10 @@
     return hasVisibility(Visibility.PRIVATE);
   }
 
+  public static <T extends MemberSubject> Matcher<T> isPackagePrivate() {
+    return hasVisibility(Visibility.PACKAGE_PRIVATE);
+  }
+
   public static <T extends MemberSubject> Matcher<T> isPublic() {
     return hasVisibility(Visibility.PUBLIC);
   }
diff --git a/tools/r8_release.py b/tools/r8_release.py
index dce7dad..7f27da8 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -308,6 +308,12 @@
       'g4 change --desc "Update R8 to version %s\n"' % (version),
       shell=True)
 
+def get_cl_id(c4_change_output):
+  startIndex = c4_change_output.find('Change ') + len('Change ')
+  endIndex = c4_change_output.find(' ', startIndex)
+  cl = c4_change_output[startIndex:endIndex]
+  assert cl.isdigit()
+  return cl
 
 def sed(pattern, replace, path):
   with open(path, "r") as sources:
@@ -386,7 +392,11 @@
       assert options.version in blaze_result
 
       if not options.no_upload:
-        return g4_change(options.version)
+        change_result = g4_change(options.version)
+        change_result += 'Run \'(g4d ' + args.p4_client \
+                         + ' && tap_presubmit -p all --train -c ' \
+                         + get_cl_id(change_result) + ')\' for running TAP presubmit.'
+        return change_result
 
   return release_google3