Merge commit '7960737f4d8b026cc8d59c624b6300b24ba9ad04' into dev-release
diff --git a/build.gradle b/build.gradle
index 3101886..cc51ea6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -437,7 +437,8 @@
         "protobuf-lite",
         "retrace_internal",
         "youtube/youtube.android_15.33",
-        "youtube/youtube.android_16.12"
+        "youtube/youtube.android_16.12",
+        "youtube/youtube.android_16.20"
     ],
 ]
 
diff --git a/compatibility-faq.md b/compatibility-faq.md
index f3dde4e..3ba23c8 100644
--- a/compatibility-faq.md
+++ b/compatibility-faq.md
@@ -1,10 +1,38 @@
-# R8 compatibility FAQ
+# R8 FAQ
 
 R8 uses the same configuration specification language as ProGuard, and tries to
 be compatible with ProGuard. However as R8 has different optimizations it can be
-necessary to change the configuration when switching to R8.
+necessary to change the configuration when switching to R8. R8 provides two
+modes, R8 compatibility mode and R8 full mode. R8 compatibility mode is default
+in Android Studio and is meant to make the transition to R8 from ProGuard easier
+by limiting the optimizations performed by R8.
 
-This FAQ collects some of the common issues.
+## R8 full mode
+In non-compat mode, also called “full mode”, R8 performs more aggressive
+optimizations, meaning additional ProGuard configuration rules may be required.
+Full mode can be enabled by adding `android.enableR8.fullMode=true` in the
+`gradle.properties` file. The main differences compared to R8 compatibility mode
+are:
+
+- The default constructor (`<init>()`) is not implicitly kept when a class is
+kept.
+- The default constructor (`<init>()`) is not implicitly kept for types which
+are only used with `ldc`, `instanceof` or `checkcast`.
+- The enclosing classes of fields or methods that are matched by a
+`-keepclassmembers` rule are not implicitly considered to be instantiated.
+Classes that are only instantiated using reflection should be kept explicitly
+with a `-keep` rule.
+- Default methods are not implicitly kept as abstract methods.
+- Attributes (such as `Signature`) and annotations are only kept for classes,
+methods and fields which are matched by keep rules even when `-keepattributes`
+is specified.
+Additionally, for attributes describing a relationship such as `InnerClass` and
+`EnclosingMethod`, non-compat mode requires both endpoints being kept.
+
+# Troubleshooting
+
+The rest of this document describes known issues with libraries that use
+reflection.
 
 ## GSON
 
@@ -21,7 +49,7 @@
 
 ```
 -keepclassmembers,allowobfuscation class * {
-  @com.google.gson.annotations.SerializedName <fields>;
+ @com.google.gson.annotations.SerializedName <fields>;
 }
 ```
 
@@ -35,7 +63,7 @@
 
 ```
 -keepclassmembers class MyDataClass {
-  !transient <fields>;
+ !transient <fields>;
 }
 ```
 
@@ -46,15 +74,15 @@
 ### Error `java.lang.IllegalArgumentException: class <class name> declares multiple JSON fields named <name>`
 
 This can be caused by obfuscation selecting the same name for private fields in
-several classes in a class hierachy. Consider the following example:
+several classes in a class hierarchy. Consider the following example:
 
 ```
 class A {
-  private String fieldInA;
+ private String fieldInA;
 }
 
 class B extends A {
-  private String fieldInB;
+ private String fieldInB;
 }
 ```
 
@@ -66,33 +94,33 @@
 
 ```
 class A {
-  private transient String fieldInA;
+ private transient String fieldInA;
 }
 
 class B extends A {
-  private transient String fieldInB;
+ private transient String fieldInB;
 }
 ```
 
 If the fields _are_ to be serialized, the annotation `SerializedName` can be
-used to fix the `IllegalArgumentException` together the rule to keep fields
+used to fix the `IllegalArgumentException` together with the rule to keep fields
 annotated with `SerializedName`
 
 ```
 class A {
-  @SerializedName("fieldInA")
-  private String fieldInA;
+ @SerializedName("fieldInA")
+ private String fieldInA;
 }
 
 class B extends A {
-  @SerializedName("fieldInB")
-  private String fieldInB;
+ @SerializedName("fieldInB")
+ private String fieldInB;
 }
 ```
 
 ```
 -keepclassmembers,allowobfuscation class * {
-  @com.google.gson.annotations.SerializedName <fields>;
+ @com.google.gson.annotations.SerializedName <fields>;
 }
 ```
 
@@ -101,15 +129,26 @@
 the fields to be renamed by R8 to the same name, but GSON serialization will
 work as expected.
 
-# R8 full mode
+### GSON with full mode
 
-In full mode, R8 performs more aggressive optimizations, meaning that additional
-ProGuard configuration rules may be required. This section highlights some
-common issues that have been seen when using full mode.
+GSON uses type tokens to serialize and deserialize generic types.
+
+```TypeToken<List<String>> listOfStrings = new TypeToken<List<String>>() {};```
+
+The anonymous class will have a generic signature argument of `List<String>` to
+the super type `TypeToken` that is reflective read for serialization. It
+is therefore necessary to keep both the `Signature` attribute, the
+`com.google.gson.reflect.TypeToken` class and all sub-types:
+
+```
+-keepattributes Signature
+-keep class com.google.gson.reflect.TypeToken { *; }
+-keep class * extends com.google.gson.reflect.TypeToken
+```
 
 ## Retrofit
 
-### Object instantiated with Retrofit's `create()` method is always replaced with `null`
+### Objects instantiated with Retrofit's `create()` method are always replaced with `null`
 
 This happens because Retrofit uses reflection to return an object that
 implements a given interface. The issue can be resolved by using the most recent
@@ -117,3 +156,18 @@
 
 See also https://github.com/square/retrofit/issues/3005 ("Insufficient keep
 rules for R8 in full mode").
+
+### Kotlin suspend functions and generic signatures
+
+For Kotlin suspend functions the generic signature is reflectively read.
+Therefore keeping the `Signature` attribute is necessary. Full mode only keeps
+the signature for kept classes thus a keep on `kotlin.coroutines.Continuation` in
+addition to a keep on the api classes is needed:
+```
+-keepattributes Signature
+-keep class kotlin.coroutines.Continuation
+```
+
+This should be included automatically from versions built after the pull-request
+https://github.com/square/retrofit/pull/3563
+
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7bb927c..844d01a 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -930,12 +930,10 @@
                 : AssertionTransformation.DISABLE,
             getAssertionsConfiguration());
 
-    // When generating class files the build is "intermediate" and we cannot pollute the namespace
-    // with the a hard-coded outline / enum unboxing utility class. Doing so would prohibit
-    // subsequent merging of two R8 produced libraries.
+    // TODO(b/171552739): Enable class merging for CF. When compiling libraries, we need to be
+    //  careful when merging a public member 'm' from a class A into another class B, since B could
+    //  have a kept subclass, in which case 'm' would leak into the public API.
     if (internal.isGeneratingClassFiles()) {
-      internal.outline.enabled = false;
-      internal.enableEnumUnboxing = false;
       horizontalClassMergerOptions.disable();
     }
 
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinker.java b/src/main/java/com/android/tools/r8/ResourceShrinker.java
index 77abde6..bf4f471 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinker.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinker.java
@@ -47,6 +47,8 @@
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.ir.code.SingleConstant;
 import com.android.tools.r8.ir.code.WideConstant;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
@@ -140,6 +142,14 @@
     void referencedStaticField(String internalName, String fieldName);
 
     void referencedMethod(String internalName, String methodName, String methodDescriptor);
+
+    default void startMethodVisit(MethodReference methodReference) {}
+
+    default void endMethodVisit(MethodReference methodReference) {}
+
+    default void startClassVisit(ClassReference classReference) {}
+
+    default void endClassVisit(ClassReference classReference) {}
   }
 
   private static final class DexClassUsageVisitor {
@@ -153,6 +163,7 @@
     }
 
     public void visit() {
+      callback.startClassVisit(classDef.getClassReference());
       if (!callback.shouldProcess(classDef.type.getInternalName())) {
         return;
       }
@@ -165,12 +176,16 @@
       }
 
       for (DexEncodedMethod method : classDef.allMethodsSorted()) {
+
+        callback.startMethodVisit(method.getReference().asMethodReference());
         processMethod(method);
+        callback.endMethodVisit(method.getReference().asMethodReference());
       }
 
       if (classDef.hasClassOrMemberAnnotations()) {
         processAnnotations(classDef);
       }
+      callback.endClassVisit(classDef.getClassReference());
     }
 
     private void processFieldValue(DexValue value) {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index 592ee31..cd8d6b7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -42,6 +42,10 @@
     spec.withInt(f -> f.opcode).withItem(f -> f.field).withItem(f -> f.declaringField);
   }
 
+  public CfFieldInstruction(int opcode, DexField field) {
+    this(opcode, field, field);
+  }
+
   public CfFieldInstruction(int opcode, DexField field, DexField declaringField) {
     this.opcode = opcode;
     this.field = field;
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 4ef90be..6ef4c6a 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -67,7 +67,7 @@
   private ProguardCompatibilityActions proguardCompatibilityActions;
   private RootSet rootSet;
   private MainDexRootSet mainDexRootSet = null;
-  // This should perferably always be obtained via AppInfoWithLiveness.
+  // This should preferably always be obtained via AppInfoWithLiveness.
   // Currently however the liveness may be downgraded thus loosing the computed keep info.
   private KeepInfoCollection keepInfo = null;
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index ada1c8a..1e10fa9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.structural.StructuralMapping;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 public class DexEncodedField extends DexEncodedMember<DexEncodedField, DexField>
     implements StructuralItem<DexEncodedField> {
@@ -116,18 +117,15 @@
     return false;
   }
 
+  @Override
   public FieldOptimizationInfo getOptimizationInfo() {
     return optimizationInfo;
   }
 
   public synchronized MutableFieldOptimizationInfo getMutableOptimizationInfo() {
-    if (optimizationInfo.isDefaultFieldOptimizationInfo()) {
-      MutableFieldOptimizationInfo mutableOptimizationInfo = new MutableFieldOptimizationInfo();
-      optimizationInfo = mutableOptimizationInfo;
-      return mutableOptimizationInfo;
-    }
-    assert optimizationInfo.isMutableFieldOptimizationInfo();
-    return optimizationInfo.asMutableFieldOptimizationInfo();
+    MutableFieldOptimizationInfo mutableInfo = optimizationInfo.toMutableOptimizationInfo();
+    optimizationInfo = mutableInfo;
+    return mutableInfo;
   }
 
   public void setOptimizationInfo(MutableFieldOptimizationInfo info) {
@@ -197,6 +195,12 @@
     return asProgramField(definitions);
   }
 
+  @Override
+  public <T> T apply(
+      Function<DexEncodedField, T> fieldConsumer, Function<DexEncodedMethod, T> methodConsumer) {
+    return fieldConsumer.apply(this);
+  }
+
   public ProgramField asProgramField(DexDefinitionSupplier definitions) {
     assert getHolderType().isClassType();
     DexProgramClass clazz = asProgramClassOrNull(definitions.definitionForHolder(getReference()));
@@ -376,9 +380,9 @@
       annotations = from.annotations();
       staticValue = from.staticValue;
       optimizationInfo =
-          from.optimizationInfo.isDefaultFieldOptimizationInfo()
-              ? DefaultFieldOptimizationInfo.getInstance()
-              : from.optimizationInfo.mutableCopy();
+          from.optimizationInfo.isMutableOptimizationInfo()
+              ? from.optimizationInfo.asMutableFieldOptimizationInfo().mutableCopy()
+              : from.optimizationInfo;
       deprecated = from.isDeprecated();
       d8R8Synthesized = from.isD8R8Synthesized();
     }
@@ -416,9 +420,7 @@
               staticValue,
               deprecated,
               d8R8Synthesized);
-      if (optimizationInfo.isMutableFieldOptimizationInfo()) {
-        dexEncodedField.setOptimizationInfo(optimizationInfo.asMutableFieldOptimizationInfo());
-      }
+      dexEncodedField.optimizationInfo = optimizationInfo;
       buildConsumer.accept(dexEncodedField);
       return dexEncodedField;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
index e1bb25e..271fd68 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.ir.optimize.info.MemberOptimizationInfo;
 import com.android.tools.r8.kotlin.KotlinMemberLevelInfo;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 public abstract class DexEncodedMember<D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
     extends DexDefinition {
@@ -63,6 +66,24 @@
 
   public abstract ProgramMember<D, R> asProgramMember(DexDefinitionSupplier definitions);
 
+  public abstract <T> T apply(
+      Function<DexEncodedField, T> fieldConsumer, Function<DexEncodedMethod, T> methodConsumer);
+
+  public void apply(
+      Consumer<DexEncodedField> fieldConsumer, Consumer<DexEncodedMethod> methodConsumer) {
+    apply(
+        field -> {
+          fieldConsumer.accept(field);
+          return null;
+        },
+        method -> {
+          methodConsumer.accept(method);
+          return null;
+        });
+  }
+
+  public abstract MemberOptimizationInfo<?> getOptimizationInfo();
+
   @Override
   public final boolean equals(Object other) {
     if (other == this) {
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 434b091..449747b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -28,7 +28,7 @@
 import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.cf.code.CfThrow;
-import com.android.tools.r8.code.Const;
+import com.android.tools.r8.code.Const4;
 import com.android.tools.r8.code.ConstString;
 import com.android.tools.r8.code.InstanceOf;
 import com.android.tools.r8.code.Instruction;
@@ -58,8 +58,8 @@
 import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationWithMinApiInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
-import com.android.tools.r8.ir.optimize.info.UpdatableMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.FieldAccessorBuilder;
@@ -93,6 +93,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.IntPredicate;
 import org.objectweb.asm.Opcodes;
 
@@ -436,6 +437,12 @@
     return asProgramMethod(definitions);
   }
 
+  @Override
+  public <T> T apply(
+      Function<DexEncodedField, T> fieldConsumer, Function<DexEncodedMethod, T> methodConsumer) {
+    return methodConsumer.apply(this);
+  }
+
   public DexClassAndMethod asDexClassAndMethod(DexDefinitionSupplier definitions) {
     assert getReference().holder.isClassType();
     DexClass clazz = definitions.definitionForHolder(getReference());
@@ -1011,7 +1018,7 @@
   }
 
   public DexCode buildEmptyThrowingDexCode() {
-    Instruction[] insn = {new Const(0, 0), new Throw(0)};
+    Instruction[] insn = {new Const4(0, 0), new Throw(0)};
     return generateCodeFromTemplate(1, 0, insn);
   }
 
@@ -1445,20 +1452,20 @@
     return m1.getReference().compareTo(m2.getReference());
   }
 
+  @Override
   public MethodOptimizationInfo getOptimizationInfo() {
     checkIfObsolete();
     return optimizationInfo;
   }
 
-  public synchronized UpdatableMethodOptimizationInfo getMutableOptimizationInfo() {
+  public synchronized MutableMethodOptimizationInfo getMutableOptimizationInfo() {
     checkIfObsolete();
-    UpdatableMethodOptimizationInfo updatableMethodOptimizationInfo =
-        optimizationInfo.asUpdatableMethodOptimizationInfo();
-    this.optimizationInfo = updatableMethodOptimizationInfo;
-    return updatableMethodOptimizationInfo;
+    MutableMethodOptimizationInfo mutableInfo = optimizationInfo.toMutableOptimizationInfo();
+    optimizationInfo = mutableInfo;
+    return mutableInfo;
   }
 
-  public void setOptimizationInfo(UpdatableMethodOptimizationInfo info) {
+  public void setOptimizationInfo(MutableMethodOptimizationInfo info) {
     checkIfObsolete();
     optimizationInfo = info;
   }
@@ -1542,9 +1549,9 @@
       code = from.code;
       compilationState = CompilationState.NOT_PROCESSED;
       optimizationInfo =
-          from.optimizationInfo.isDefaultMethodOptimizationInfo()
-              ? DefaultMethodOptimizationInfo.getInstance()
-              : from.optimizationInfo.mutableCopy();
+          from.optimizationInfo.isMutableOptimizationInfo()
+              ? from.optimizationInfo.asMutableMethodOptimizationInfo().mutableCopy()
+              : from.optimizationInfo;
       kotlinMemberInfo = from.kotlinMemberInfo;
       classFileVersion = from.classFileVersion;
       this.d8R8Synthesized = d8R8Synthesized;
@@ -1679,8 +1686,9 @@
     }
 
     public Builder adjustOptimizationInfoAfterRemovingThisParameter() {
-      if (optimizationInfo.isUpdatableMethodOptimizationInfo()) {
-        optimizationInfo.asUpdatableMethodOptimizationInfo()
+      if (optimizationInfo.isMutableOptimizationInfo()) {
+        optimizationInfo
+            .asMutableMethodOptimizationInfo()
             .adjustOptimizationInfoAfterRemovingThisParameter();
       }
       return this;
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 646c9a0..a42388a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -2238,6 +2238,10 @@
     return createMethod(holder, createProto(voidType), classConstructorMethodName);
   }
 
+  public DexMethod createInstanceInitializer(DexType holder, DexType... parameters) {
+    return createMethod(holder, createProto(voidType, parameters), constructorMethodName);
+  }
+
   public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, List<DexType> extraTypes, Predicate<DexMethod> isFresh) {
     assert method.isInstanceInitializer(this);
@@ -2258,8 +2262,6 @@
 
   private DexMethod createInstanceInitializerWithFreshProto(
       DexProto proto, List<DexType> extraTypes, Function<DexProto, Optional<DexMethod>> isFresh) {
-    assert !extraTypes.isEmpty();
-
     Queue<Iterable<DexProto>> tryProtos = new LinkedList<>();
     Iterator<DexProto> current = IterableUtils.singleton(proto).iterator();
 
@@ -2276,6 +2278,7 @@
       if (object.isPresent()) {
         return object.get();
       }
+      assert !extraTypes.isEmpty();
       tryProtos.add(
           Iterables.transform(extraTypes, extraType -> appendTypeToProto(tryProto, extraType)));
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index a38770a..96c3084 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -93,6 +93,11 @@
     return descriptor;
   }
 
+  public int getRequiredRegisters() {
+    assert !isVoidType();
+    return isWideType() ? 2 : 1;
+  }
+
   @Override
   public int computeHashCode() {
     return descriptor.hashCode();
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
new file mode 100644
index 0000000..b6d9757
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeUtils.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.InterfaceCollection;
+import com.android.tools.r8.utils.ObjectUtils;
+import java.util.Iterator;
+
+public class DexTypeUtils {
+
+  public static DexType computeLeastUpperBound(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Iterable<DexType> types) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+
+    Iterator<DexType> iterator = types.iterator();
+    assert iterator.hasNext();
+
+    DexType result = iterator.next();
+    while (iterator.hasNext()) {
+      result = computeLeastUpperBound(appView, result, iterator.next());
+    }
+    return result;
+  }
+
+  public static DexType computeLeastUpperBound(
+      AppView<? extends AppInfoWithClassHierarchy> appView, DexType type, DexType other) {
+    if (type == other) {
+      return type;
+    }
+
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    if (type == dexItemFactory.objectType
+        || other == dexItemFactory.objectType
+        || type.isArrayType() != other.isArrayType()) {
+      return dexItemFactory.objectType;
+    }
+
+    if (type.isArrayType()) {
+      assert other.isArrayType();
+      int arrayDimension = type.getNumberOfLeadingSquareBrackets();
+      if (other.getNumberOfLeadingSquareBrackets() != arrayDimension) {
+        return dexItemFactory.objectType;
+      }
+
+      DexType baseType = type.toBaseType(dexItemFactory);
+      DexType otherBaseType = other.toBaseType(dexItemFactory);
+      if (baseType.isPrimitiveType() || otherBaseType.isPrimitiveType()) {
+        assert baseType != otherBaseType;
+        return dexItemFactory.objectType;
+      }
+
+      return dexItemFactory.createArrayType(
+          arrayDimension, computeLeastUpperBound(appView, baseType, otherBaseType));
+    }
+
+    assert !type.isArrayType();
+    assert !other.isArrayType();
+
+    boolean isInterface =
+        type.isClassType()
+            && ObjectUtils.getBooleanOrElse(
+                appView.definitionFor(type), DexClass::isInterface, false);
+    boolean otherIsInterface =
+        other.isClassType()
+            && ObjectUtils.getBooleanOrElse(
+                appView.definitionFor(other), DexClass::isInterface, false);
+    if (isInterface != otherIsInterface) {
+      return dexItemFactory.objectType;
+    }
+
+    if (isInterface) {
+      assert otherIsInterface;
+      InterfaceCollection interfaceCollection =
+          ClassTypeElement.computeLeastUpperBoundOfInterfaces(
+              appView, InterfaceCollection.singleton(type), InterfaceCollection.singleton(other));
+      return interfaceCollection.hasSingleKnownInterface()
+          ? interfaceCollection.getSingleKnownInterface()
+          : dexItemFactory.objectType;
+    }
+
+    assert !isInterface;
+    assert !otherIsInterface;
+
+    return ClassTypeElement.computeLeastUpperBoundOfClasses(appView.appInfo(), type, other);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
index ec86d3e..a3cf3a1 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
@@ -125,15 +125,17 @@
     SignatureEvaluationResult result =
         genericSignatureContextEvaluator.evaluateClassSignatureForContext(typeParameterContext);
     if (result.isInvalid() && mode.clearIfInvalid()) {
-      appView
-          .options()
-          .reporter
-          .info(
-              GenericSignatureValidationDiagnostic.invalidClassSignature(
-                  clazz.getClassSignature().toString(),
-                  clazz.getTypeName(),
-                  clazz.getOrigin(),
-                  result));
+      if (appView.hasLiveness() && appView.getKeepInfo().getClassInfo(clazz).isPinned()) {
+        appView
+            .options()
+            .reporter
+            .info(
+                GenericSignatureValidationDiagnostic.invalidClassSignature(
+                    clazz.getClassSignature().toString(),
+                    clazz.getTypeName(),
+                    clazz.getOrigin(),
+                    result));
+      }
       clazz.clearClassSignature();
     }
     for (DexEncodedMethod method : clazz.methods()) {
@@ -145,15 +147,18 @@
                       genericSignatureContextEvaluator.visitMethodSignature(
                           methodSignature, typeParameterContext),
                   invalidResult -> {
-                    appView
-                        .options()
-                        .reporter
-                        .info(
-                            GenericSignatureValidationDiagnostic.invalidMethodSignature(
-                                method.getGenericSignature().toString(),
-                                method.toSourceString(),
-                                clazz.getOrigin(),
-                                invalidResult));
+                    if (appView.hasLiveness()
+                        && appView.getKeepInfo().getMethodInfo(method, clazz).isPinned()) {
+                      appView
+                          .options()
+                          .reporter
+                          .info(
+                              GenericSignatureValidationDiagnostic.invalidMethodSignature(
+                                  method.getGenericSignature().toString(),
+                                  method.toSourceString(),
+                                  clazz.getOrigin(),
+                                  invalidResult));
+                    }
                     method.clearGenericSignature();
                   }));
     }
@@ -166,15 +171,18 @@
                       genericSignatureContextEvaluator.visitFieldTypeSignature(
                           fieldSignature, typeParameterContext),
                   invalidResult -> {
-                    appView
-                        .options()
-                        .reporter
-                        .info(
-                            GenericSignatureValidationDiagnostic.invalidFieldSignature(
-                                field.getGenericSignature().toString(),
-                                field.toSourceString(),
-                                clazz.getOrigin(),
-                                invalidResult));
+                    if (appView.hasLiveness()
+                        && appView.getKeepInfo().getFieldInfo(field, clazz).isPinned()) {
+                      appView
+                          .options()
+                          .reporter
+                          .info(
+                              GenericSignatureValidationDiagnostic.invalidFieldSignature(
+                                  field.getGenericSignature().toString(),
+                                  field.toSourceString(),
+                                  clazz.getOrigin(),
+                                  invalidResult));
+                    }
                     field.clearGenericSignature();
                   }));
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index a429d5c..e0eaf2c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -8,10 +8,12 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens.Builder;
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -19,27 +21,28 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class ClassInstanceFieldsMerger {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final MergeGroup group;
   private final Builder lensBuilder;
 
   private DexEncodedField classIdField;
 
   // Map from target class field to all fields which should be merged into that field.
-  private final Map<DexEncodedField, List<DexEncodedField>> fieldMappings = new LinkedHashMap<>();
+  private final MutableBidirectionalManyToOneMap<DexEncodedField, DexEncodedField> fieldMappings =
+      BidirectionalManyToOneHashMap.newLinkedHashMap();
 
   public ClassInstanceFieldsMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       MergeGroup group) {
     this.appView = appView;
+    this.group = group;
     this.lensBuilder = lensBuilder;
-    group
-        .getTarget()
-        .instanceFields()
-        .forEach(field -> fieldMappings.computeIfAbsent(field, ignore -> new ArrayList<>()));
+    group.forEachSource(this::addFields);
   }
 
   /**
@@ -52,7 +55,7 @@
    * Bar has fields 'A b' and 'B a'), we make a prepass that matches fields with the same reference
    * type.
    */
-  public void addFields(DexProgramClass clazz) {
+  private void addFields(DexProgramClass clazz) {
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByExactInfo =
         getAvailableFieldsByExactInfo();
     List<DexEncodedField> needsMerge = new ArrayList<>();
@@ -66,7 +69,7 @@
         needsMerge.add(oldField);
       } else {
         DexEncodedField newField = availableFieldsWithExactSameInfo.removeFirst();
-        fieldMappings.get(newField).add(oldField);
+        fieldMappings.put(oldField, newField);
         if (availableFieldsWithExactSameInfo.isEmpty()) {
           availableFieldsByExactInfo.remove(info);
         }
@@ -84,14 +87,14 @@
               .removeFirst();
       assert newField != null;
       assert newField.getType().isReferenceType();
-      fieldMappings.get(newField).add(oldField);
+      fieldMappings.put(oldField, newField);
     }
   }
 
   private Map<InstanceFieldInfo, LinkedList<DexEncodedField>> getAvailableFieldsByExactInfo() {
     Map<InstanceFieldInfo, LinkedList<DexEncodedField>> availableFieldsByInfo =
         new LinkedHashMap<>();
-    for (DexEncodedField field : fieldMappings.keySet()) {
+    for (DexEncodedField field : group.getTarget().instanceFields()) {
       availableFieldsByInfo
           .computeIfAbsent(InstanceFieldInfo.createExact(field), ignore -> new LinkedList<>())
           .add(field);
@@ -122,6 +125,14 @@
     }
   }
 
+  public ProgramField getTargetField(ProgramField field) {
+    if (field.getHolder() == group.getTarget()) {
+      return field;
+    }
+    DexEncodedField targetField = fieldMappings.get(field.getDefinition());
+    return new ProgramField(group.getTarget(), targetField);
+  }
+
   public void setClassIdField(DexEncodedField classIdField) {
     this.classIdField = classIdField;
   }
@@ -131,19 +142,18 @@
     if (classIdField != null) {
       newFields.add(classIdField);
     }
-    fieldMappings.forEach(
-        (targetField, oldFields) ->
-            newFields.add(mergeSourceFieldsToTargetField(targetField, oldFields)));
+    fieldMappings.forEachManyToOneMapping(
+        (sourceFields, targetField) ->
+            newFields.add(mergeSourceFieldsToTargetField(targetField, sourceFields)));
     return newFields.toArray(DexEncodedField.EMPTY_ARRAY);
   }
 
   private DexEncodedField mergeSourceFieldsToTargetField(
-      DexEncodedField targetField, List<DexEncodedField> oldFields) {
-    fixAccessFlags(targetField, oldFields);
+      DexEncodedField targetField, Set<DexEncodedField> sourceFields) {
+    fixAccessFlags(targetField, sourceFields);
 
     DexEncodedField newField;
-    DexType targetFieldType = targetField.type();
-    if (!Iterables.all(oldFields, oldField -> oldField.getType() == targetFieldType)) {
+    if (needsRelaxedType(targetField, sourceFields)) {
       newField =
           targetField.toTypeSubstitutedField(
               targetField
@@ -155,10 +165,16 @@
 
     lensBuilder.recordNewFieldSignature(
         Iterables.transform(
-            IterableUtils.append(oldFields, targetField), DexEncodedField::getReference),
+            IterableUtils.append(sourceFields, targetField), DexEncodedField::getReference),
         newField.getReference(),
         targetField.getReference());
 
     return newField;
   }
+
+  private boolean needsRelaxedType(
+      DexEncodedField targetField, Iterable<DexEncodedField> sourceFields) {
+    return Iterables.any(
+        sourceFields, sourceField -> sourceField.getType() != targetField.getType());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index 89e9f74..3927d11 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -62,13 +62,18 @@
   private final Mode mode;
   private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
-  private final ClassInitializerMerger classInitializerMerger;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
 
   private final ClassMethodsBuilder classMethodsBuilder = new ClassMethodsBuilder();
   private final Reference2IntMap<DexType> classIdentifiers = new Reference2IntOpenHashMap<>();
-  private final ClassStaticFieldsMerger classStaticFieldsMerger;
+
+  // Field mergers.
   private final ClassInstanceFieldsMerger classInstanceFieldsMerger;
+  private final ClassStaticFieldsMerger classStaticFieldsMerger;
+
+  // Method mergers.
+  private final ClassInitializerMerger classInitializerMerger;
+  private final InstanceInitializerMergerCollection instanceInitializerMergers;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
 
   private ClassMerger(
@@ -82,10 +87,18 @@
     this.group = group;
     this.lensBuilder = lensBuilder;
     this.mode = mode;
-    this.virtualMethodMergers = virtualMethodMergers;
-    this.classInitializerMerger = ClassInitializerMerger.create(group);
+
+    // Field mergers.
     this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, group);
     this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(appView, lensBuilder, group);
+
+    // Method mergers.
+    this.classInitializerMerger = ClassInitializerMerger.create(group);
+    this.instanceInitializerMergers =
+        InstanceInitializerMergerCollection.create(
+            appView, classIdentifiers, group, classInstanceFieldsMerger, lensBuilder, mode);
+    this.virtualMethodMergers = virtualMethodMergers;
+
     buildClassIdentifierMap();
   }
 
@@ -175,12 +188,8 @@
   }
 
   void mergeInstanceInitializers(SyntheticArgumentClass syntheticArgumentClass) {
-    InstanceInitializerMergerCollection instanceInitializerMergers =
-        InstanceInitializerMergerCollection.create(appView, group, lensBuilder, mode);
     instanceInitializerMergers.forEach(
-        merger ->
-            merger.merge(
-                classMethodsBuilder, lensBuilder, classIdentifiers, syntheticArgumentClass));
+        merger -> merger.merge(classMethodsBuilder, syntheticArgumentClass));
   }
 
   void mergeMethods(
@@ -295,11 +304,7 @@
   }
 
   void mergeInstanceFields() {
-    group.forEachSource(
-        clazz -> {
-          classInstanceFieldsMerger.addFields(clazz);
-          clazz.clearInstanceFields();
-        });
+    group.forEachSource(DexClass::clearInstanceFields);
     group.getTarget().setInstanceFields(classInstanceFieldsMerger.merge());
   }
 
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 c05af00..4ad6f55 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -212,6 +212,9 @@
     for (ClassMerger merger : classMergers) {
       merger.mergeGroup(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
     }
+
+    // Clear type elements cache after IR building.
+    appView.dexItemFactory().clearTypeElementsCache();
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java
new file mode 100644
index 0000000..839b6d5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerCfCode.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexType;
+import java.util.List;
+
+/**
+ * Similar to CfCode, but with a marker that makes it possible to recognize this is synthesized by
+ * the horizontal class merger.
+ */
+public class HorizontalClassMergerCfCode extends CfCode {
+
+  HorizontalClassMergerCfCode(
+      DexType originalHolder,
+      int maxStack,
+      int maxLocals,
+      List<CfInstruction> instructions,
+      List<CfTryCatch> tryCatchRanges,
+      List<LocalVariableInfo> localVariables) {
+    super(originalHolder, maxStack, maxLocals, instructions, tryCatchRanges, localVariables);
+  }
+
+  @Override
+  public boolean isHorizontalClassMergingCode() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 12a4190..20eb5a3 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.NestedGraphLens;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
@@ -103,6 +104,13 @@
         AppView<?> appView, HorizontallyMergedClasses mergedClasses) {
       assert pendingMethodMapUpdates.isEmpty();
       assert pendingNewMethodSignatureUpdates.isEmpty();
+      assert newMethodSignatures.values().stream()
+          .allMatch(
+              value -> {
+                assert newMethodSignatures.getKeys(value).size() == 1
+                    || newMethodSignatures.hasExplicitRepresentativeKey(value);
+                return true;
+              });
       return new HorizontalClassMergerGraphLens(
           appView,
           mergedClasses,
@@ -112,6 +120,11 @@
           newMethodSignatures);
     }
 
+    DexMethod getRenamedMethodSignature(DexMethod method) {
+      assert newMethodSignatures.containsKey(method);
+      return newMethodSignatures.get(method);
+    }
+
     void recordNewFieldSignature(DexField oldFieldSignature, DexField newFieldSignature) {
       fieldMap.put(oldFieldSignature, newFieldSignature);
     }
@@ -157,6 +170,17 @@
       recordNewMethodSignature(from, to, isRepresentative);
     }
 
+    void moveMethods(Iterable<ProgramMethod> methods, DexMethod to) {
+      moveMethods(methods, to, null);
+    }
+
+    void moveMethods(Iterable<ProgramMethod> methods, DexMethod to, ProgramMethod representative) {
+      for (ProgramMethod from : methods) {
+        boolean isRepresentative = representative != null && from == representative;
+        moveMethod(from.getReference(), to, isRepresentative);
+      }
+    }
+
     void recordNewMethodSignature(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
       recordNewMethodSignature(oldMethodSignature, newMethodSignature, false);
     }
@@ -194,6 +218,10 @@
         for (DexMethod originalMethodSignature : oldMethodSignatures) {
           pendingNewMethodSignatureUpdates.put(originalMethodSignature, newMethodSignature);
         }
+        DexMethod representative = newMethodSignatures.getRepresentativeKey(oldMethodSignature);
+        if (representative != null) {
+          pendingNewMethodSignatureUpdates.setRepresentative(newMethodSignature, representative);
+        }
       }
     }
 
@@ -205,7 +233,13 @@
 
       // Commit pending original method signatures updates.
       newMethodSignatures.removeAll(pendingNewMethodSignatureUpdates.keySet());
-      pendingNewMethodSignatureUpdates.forEachManyToOneMapping(newMethodSignatures::put);
+      pendingNewMethodSignatureUpdates.forEachManyToOneMapping(
+          (keys, value, representative) -> {
+            newMethodSignatures.put(keys, value);
+            if (keys.size() > 1) {
+              newMethodSignatures.setRepresentative(value, representative);
+            }
+          });
       pendingNewMethodSignatureUpdates.clear();
     }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
index febdbe6..9a6390a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerAnalysis.java
@@ -4,14 +4,204 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import static com.android.tools.r8.ir.code.Opcodes.ARGUMENT;
+import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
+import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.GOTO;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.RETURN;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstancePut;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.List;
 
 public class InstanceInitializerAnalysis {
 
   public static InstanceInitializerDescription analyze(
-      ProgramMethod instanceInitializer, HorizontalClassMergerGraphLens.Builder lensBuilder) {
-    // TODO(b/189296638): Return an InstanceInitializerDescription if the given instance initializer
-    //  is parent constructor call followed by a sequence of instance field puts.
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      MergeGroup group,
+      ProgramMethod instanceInitializer,
+      ClassInstanceFieldsMerger instanceFieldsMerger,
+      Mode mode) {
+    InstanceInitializerDescription.Builder builder =
+        InstanceInitializerDescription.builder(appView, instanceInitializer);
+
+    if (mode.isFinal()) {
+      // TODO(b/189296638): We can't build IR in the final round of class merging without simulating
+      //  that we're in D8.
+      MethodOptimizationInfo optimizationInfo =
+          instanceInitializer.getDefinition().getOptimizationInfo();
+      if (optimizationInfo.mayHaveSideEffects()) {
+        return null;
+      }
+      InstanceInitializerInfo instanceInitializerInfo =
+          optimizationInfo.getContextInsensitiveInstanceInitializerInfo();
+      if (!instanceInitializerInfo.hasParent()) {
+        // We don't know the parent constructor of the first constructor.
+        return null;
+      }
+      DexMethod parent = instanceInitializerInfo.getParent();
+      if (parent.getArity() > 0) {
+        return null;
+      }
+      builder.addInvokeConstructor(parent, ImmutableList.of());
+      return builder.build();
+    }
+
+    IRCode code = instanceInitializer.buildIR(appView);
+    WorkList<BasicBlock> workList = WorkList.newIdentityWorkList(code.entryBlock());
+    while (workList.hasNext()) {
+      BasicBlock block = workList.next();
+      for (Instruction instruction : block.getInstructions()) {
+        switch (instruction.opcode()) {
+          case ARGUMENT:
+          case ASSUME:
+          case CONST_CLASS:
+          case CONST_NUMBER:
+          case CONST_STRING:
+          case DEX_ITEM_BASED_CONST_STRING:
+          case RETURN:
+            break;
+
+          case GOTO:
+            if (!workList.addIfNotSeen(instruction.asGoto().getTarget())) {
+              return invalid();
+            }
+            break;
+
+          case INSTANCE_PUT:
+            {
+              InstancePut instancePut = instruction.asInstancePut();
+
+              // This must initialize a field on the receiver.
+              if (!instancePut.object().getAliasedValue().isThis()) {
+                return invalid();
+              }
+
+              // Check that this writes a field on the enclosing class.
+              DexField fieldReference = instancePut.getField();
+              DexField lensRewrittenFieldReference =
+                  appView.graphLens().lookupField(fieldReference);
+              if (lensRewrittenFieldReference.getHolderType()
+                  != instanceInitializer.getHolderType()) {
+                return invalid();
+              }
+
+              ProgramField sourceField =
+                  instanceInitializer.getHolder().lookupProgramField(lensRewrittenFieldReference);
+              if (sourceField == null) {
+                return invalid();
+              }
+
+              InstanceFieldInitializationInfo initializationInfo =
+                  getInitializationInfo(instancePut.value(), appView, instanceInitializer);
+              if (initializationInfo == null) {
+                return invalid();
+              }
+
+              ProgramField targetField = instanceFieldsMerger.getTargetField(sourceField);
+              assert targetField != null;
+
+              builder.addInstancePut(targetField.getReference(), initializationInfo);
+              break;
+            }
+
+          case INVOKE_DIRECT:
+            {
+              InvokeDirect invoke = instruction.asInvokeDirect();
+
+              // This must initialize the receiver.
+              if (!invoke.getReceiver().getAliasedValue().isThis()) {
+                return invalid();
+              }
+
+              DexMethod invokedMethod = invoke.getInvokedMethod();
+              DexMethod lensRewrittenInvokedMethod =
+                  appView
+                      .graphLens()
+                      .lookupInvokeDirect(invokedMethod, instanceInitializer)
+                      .getReference();
+
+              // TODO(b/189296638): Consider allowing constructor forwarding.
+              if (!lensRewrittenInvokedMethod.isInstanceInitializer(appView.dexItemFactory())
+                  || lensRewrittenInvokedMethod.getHolderType() != group.getSuperType()) {
+                return invalid();
+              }
+
+              // Extract the non-receiver arguments.
+              List<InstanceFieldInitializationInfo> arguments =
+                  new ArrayList<>(invoke.arguments().size() - 1);
+              for (Value argument : Iterables.skip(invoke.arguments(), 1)) {
+                InstanceFieldInitializationInfo initializationInfo =
+                    getInitializationInfo(argument, appView, instanceInitializer);
+                if (initializationInfo == null) {
+                  return invalid();
+                }
+                arguments.add(initializationInfo);
+              }
+
+              if (!builder.addInvokeConstructor(invokedMethod, arguments)) {
+                return invalid();
+              }
+            }
+            break;
+
+          default:
+            // Not allowed.
+            return invalid();
+        }
+      }
+    }
+
+    return builder.isValid() ? builder.build() : null;
+  }
+
+  private static InstanceFieldInitializationInfo getInitializationInfo(
+      Value value,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      ProgramMethod instanceInitializer) {
+    InstanceFieldInitializationInfoFactory factory =
+        appView.instanceFieldInitializationInfoFactory();
+
+    Value root = value.getAliasedValue();
+    if (root.isDefinedByInstructionSatisfying(Instruction::isArgument)) {
+      return factory.createArgumentInitializationInfo(
+          value.getDefinition().asArgument().getIndex());
+    }
+
+    AbstractValue abstractValue = value.getAbstractValue(appView, instanceInitializer);
+    if (abstractValue.isSingleConstValue()) {
+      return abstractValue.asSingleConstValue();
+    }
+
+    return null;
+  }
+
+  private static InstanceInitializerDescription invalid() {
     return null;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
index 41f2c4e..b67c652 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerDescription.java
@@ -4,14 +4,44 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfDexItemBasedConstString;
+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.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.InvokeDirectRange;
+import com.android.tools.r8.code.ReturnVoid;
+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.DexCode;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.value.SingleConstValue;
+import com.android.tools.r8.ir.analysis.value.SingleDexItemBasedStringValue;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.IterableUtils;
+import com.google.common.collect.ImmutableList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import org.objectweb.asm.Opcodes;
 
 /**
  * A simple abstraction of an instance initializer's code, which allows a parent constructor call
@@ -19,24 +49,219 @@
  */
 public class InstanceInitializerDescription {
 
-  private final int arity;
-  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments;
+  // Field assignments that happen prior to the parent constructor call.
+  //
+  // Most fields are generally assigned after the parent constructor call, but both javac and
+  // kotlinc may assign instance fields prior to the parent constructor call. For example, the
+  // synthetic this$0 field for non-static inner classes is typically assigned prior to the parent
+  // constructor call.
+  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre;
+
+  // Field assignments that happens after the parent constructor call.
+  private final Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost;
+
+  // The parent constructor method and the arguments passed to it.
   private final DexMethod parentConstructor;
   private final List<InstanceFieldInitializationInfo> parentConstructorArguments;
 
+  // The constructor parameters, where reference types have been mapped to java.lang.Object, to
+  // ensure we don't group constructors such as <init>(int) and <init>(Object), since this would
+  // lead to type errors.
+  private final DexTypeList relaxedParameters;
+
   InstanceInitializerDescription(
-      int arity,
-      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost,
       DexMethod parentConstructor,
-      List<InstanceFieldInitializationInfo> parentConstructorArguments) {
-    this.arity = arity;
-    this.instanceFieldAssignments = instanceFieldAssignments;
+      List<InstanceFieldInitializationInfo> parentConstructorArguments,
+      DexTypeList relaxedParameters) {
+    this.instanceFieldAssignmentsPre = instanceFieldAssignmentsPre;
+    this.instanceFieldAssignmentsPost = instanceFieldAssignmentsPost;
     this.parentConstructor = parentConstructor;
     this.parentConstructorArguments = parentConstructorArguments;
+    this.relaxedParameters = relaxedParameters;
   }
 
-  public static Builder builder(ProgramMethod instanceInitializer) {
-    return new Builder(instanceInitializer);
+  public static Builder builder(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod instanceInitializer) {
+    return new Builder(appView.dexItemFactory(), instanceInitializer);
+  }
+
+  /**
+   * Transform this description into actual CF code.
+   *
+   * @param newMethodReference the reference of the method that is being synthesized
+   * @param originalMethodReference the original reference of the representative method
+   * @param syntheticMethodReference the original, synthetic reference of the new method reference
+   *     ($r8$init$synthetic)
+   */
+  public CfCode createCfCode(
+      DexMethod newMethodReference,
+      DexMethod originalMethodReference,
+      DexMethod syntheticMethodReference,
+      MergeGroup group,
+      boolean hasClassId,
+      int extraNulls) {
+    int[] argumentToLocalIndex =
+        new int[newMethodReference.getParameters().size() + 1 - extraNulls];
+    int maxLocals = 0;
+    argumentToLocalIndex[0] = maxLocals++;
+    for (int i = 1; i < argumentToLocalIndex.length; i++) {
+      argumentToLocalIndex[i] = maxLocals;
+      maxLocals += newMethodReference.getParameter(i - 1).getRequiredRegisters();
+    }
+
+    IntBox maxStack = new IntBox();
+    ImmutableList.Builder<CfInstruction> instructionBuilder = ImmutableList.builder();
+
+    // Set position.
+    Position callerPosition = Position.synthetic(0, syntheticMethodReference, null);
+    Position calleePosition = Position.synthetic(0, originalMethodReference, callerPosition);
+    CfPosition position = new CfPosition(new CfLabel(), calleePosition);
+    instructionBuilder.add(position);
+    instructionBuilder.add(position.getLabel());
+
+    // Assign class id.
+    if (group.hasClassIdField()) {
+      assert hasClassId;
+      int classIdLocalIndex = maxLocals - 1;
+      instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+      instructionBuilder.add(new CfLoad(ValueType.INT, classIdLocalIndex));
+      instructionBuilder.add(new CfFieldInstruction(Opcodes.PUTFIELD, group.getClassIdField()));
+      maxStack.set(2);
+    } else {
+      assert !hasClassId;
+    }
+
+    // Assign each field.
+    addCfInstructionsForInstanceFieldAssignments(
+        instructionBuilder, instanceFieldAssignmentsPre, argumentToLocalIndex, maxStack);
+
+    // Load receiver for parent constructor call.
+    int stackHeightForParentConstructorCall = 1;
+    instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+
+    // Load constructor arguments.
+    int i = 0;
+    for (InstanceFieldInitializationInfo initializationInfo : parentConstructorArguments) {
+      stackHeightForParentConstructorCall +=
+          addCfInstructionsForInitializationInfo(
+              instructionBuilder,
+              initializationInfo,
+              argumentToLocalIndex,
+              parentConstructor.getParameter(i));
+      i++;
+    }
+
+    // Invoke parent constructor.
+    instructionBuilder.add(new CfInvoke(Opcodes.INVOKESPECIAL, parentConstructor, false));
+    maxStack.setMax(stackHeightForParentConstructorCall);
+
+    // Assign each field.
+    addCfInstructionsForInstanceFieldAssignments(
+        instructionBuilder, instanceFieldAssignmentsPost, argumentToLocalIndex, maxStack);
+
+    // Return.
+    instructionBuilder.add(new CfReturnVoid());
+
+    return new HorizontalClassMergerCfCode(
+        newMethodReference.getHolderType(),
+        maxStack.get(),
+        maxLocals,
+        instructionBuilder.build(),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
+  private static void addCfInstructionsForInstanceFieldAssignments(
+      ImmutableList.Builder<CfInstruction> instructionBuilder,
+      Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments,
+      int[] argumentToLocalIndex,
+      IntBox maxStack) {
+    instanceFieldAssignments.forEach(
+        (field, initializationInfo) -> {
+          // Load the receiver, the field value, and then set the field.
+          instructionBuilder.add(new CfLoad(ValueType.OBJECT, 0));
+          int stackSizeForInitializationInfo =
+              addCfInstructionsForInitializationInfo(
+                  instructionBuilder, initializationInfo, argumentToLocalIndex, field.getType());
+          instructionBuilder.add(new CfFieldInstruction(Opcodes.PUTFIELD, field));
+          maxStack.setMax(stackSizeForInitializationInfo + 1);
+        });
+  }
+
+  private static int addCfInstructionsForInitializationInfo(
+      ImmutableList.Builder<CfInstruction> instructionBuilder,
+      InstanceFieldInitializationInfo initializationInfo,
+      int[] argumentToLocalIndex,
+      DexType type) {
+    if (initializationInfo.isArgumentInitializationInfo()) {
+      int argumentIndex = initializationInfo.asArgumentInitializationInfo().getArgumentIndex();
+      instructionBuilder.add(
+          new CfLoad(ValueType.fromDexType(type), argumentToLocalIndex[argumentIndex]));
+      return type.getRequiredRegisters();
+    }
+
+    assert initializationInfo.isSingleValue();
+    assert initializationInfo.asSingleValue().isSingleConstValue();
+
+    SingleConstValue singleConstValue = initializationInfo.asSingleValue().asSingleConstValue();
+    if (singleConstValue.isSingleConstClassValue()) {
+      instructionBuilder.add(
+          new CfConstClass(singleConstValue.asSingleConstClassValue().getType()));
+      return 1;
+    } else if (singleConstValue.isSingleDexItemBasedStringValue()) {
+      SingleDexItemBasedStringValue dexItemBasedStringValue =
+          singleConstValue.asSingleDexItemBasedStringValue();
+      instructionBuilder.add(
+          new CfDexItemBasedConstString(
+              dexItemBasedStringValue.getItem(), dexItemBasedStringValue.getNameComputationInfo()));
+      return 1;
+    } else if (singleConstValue.isSingleNumberValue()) {
+      if (type.isReferenceType()) {
+        assert singleConstValue.isNull();
+        instructionBuilder.add(new CfConstNull());
+        return 1;
+      } else {
+        instructionBuilder.add(
+            new CfConstNumber(
+                singleConstValue.asSingleNumberValue().getValue(), ValueType.fromDexType(type)));
+        return type.getRequiredRegisters();
+      }
+    } else {
+      assert singleConstValue.isSingleStringValue();
+      instructionBuilder.add(
+          new CfConstString(singleConstValue.asSingleStringValue().getDexString()));
+      return 1;
+    }
+  }
+
+  public DexCode createDexCode(
+      DexMethod newMethodReference,
+      DexMethod originalMethodReference,
+      DexMethod syntheticMethodReference,
+      MergeGroup group,
+      boolean hasClassId,
+      int extraNulls) {
+    assert !hasClassId;
+    assert extraNulls == 0;
+    assert parentConstructorArguments.isEmpty();
+    assert instanceFieldAssignmentsPre.isEmpty();
+    assert instanceFieldAssignmentsPost.isEmpty();
+    Instruction[] instructions = new Instruction[2];
+    instructions[0] = new InvokeDirectRange(0, 1, parentConstructor);
+    instructions[1] = new ReturnVoid();
+    int incomingRegisterSize =
+        1 + IterableUtils.sumInt(newMethodReference.getParameters(), DexType::getRequiredRegisters);
+    int outgoingRegisterSize = 1;
+    return new DexCode(
+        incomingRegisterSize,
+        incomingRegisterSize,
+        outgoingRegisterSize,
+        instructions,
+        new DexCode.Try[0],
+        new DexCode.TryHandler[0],
+        null);
   }
 
   @Override
@@ -45,8 +270,8 @@
       return false;
     }
     InstanceInitializerDescription description = (InstanceInitializerDescription) obj;
-    return arity == description.arity
-        && instanceFieldAssignments.equals(description.instanceFieldAssignments)
+    return instanceFieldAssignmentsPre.equals(description.instanceFieldAssignmentsPre)
+        && instanceFieldAssignmentsPost.equals(description.instanceFieldAssignmentsPost)
         && parentConstructor == description.parentConstructor
         && parentConstructorArguments.equals(description.parentConstructorArguments);
   }
@@ -54,38 +279,67 @@
   @Override
   public int hashCode() {
     return Objects.hash(
-        arity, instanceFieldAssignments, parentConstructor, parentConstructorArguments);
+        instanceFieldAssignmentsPre,
+        instanceFieldAssignmentsPost,
+        parentConstructor,
+        parentConstructorArguments,
+        relaxedParameters);
   }
 
   public static class Builder {
 
-    private final int arity;
+    private final DexItemFactory dexItemFactory;
+    private final DexTypeList relaxedParameters;
 
-    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignments =
+    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPre =
+        new LinkedHashMap<>();
+    private Map<DexField, InstanceFieldInitializationInfo> instanceFieldAssignmentsPost =
         new LinkedHashMap<>();
     private DexMethod parentConstructor;
     private List<InstanceFieldInitializationInfo> parentConstructorArguments;
 
-    Builder(ProgramMethod method) {
-      this.arity = method.getReference().getArity();
+    Builder(DexItemFactory dexItemFactory, ProgramMethod method) {
+      this.dexItemFactory = dexItemFactory;
+      this.relaxedParameters =
+          method
+              .getParameters()
+              .map(
+                  parameter -> parameter.isPrimitiveType() ? parameter : dexItemFactory.objectType);
     }
 
     public void addInstancePut(DexField field, InstanceFieldInitializationInfo value) {
-      instanceFieldAssignments.put(field, value);
+      // If the parent constructor is java.lang.Object.<init>() then group all the field assignments
+      // before the parent constructor call to allow more sharing.
+      if (parentConstructor == null
+          || parentConstructor == dexItemFactory.objectMembers.constructor) {
+        instanceFieldAssignmentsPre.put(field, value);
+      } else {
+        instanceFieldAssignmentsPost.put(field, value);
+      }
     }
 
-    public void addInvokeConstructor(
+    public boolean addInvokeConstructor(
         DexMethod method, List<InstanceFieldInitializationInfo> arguments) {
-      assert parentConstructor == null;
-      assert parentConstructorArguments == null;
-      parentConstructor = method;
-      parentConstructorArguments = arguments;
+      if (parentConstructor == null) {
+        parentConstructor = method;
+        parentConstructorArguments = arguments;
+        return true;
+      }
+      return false;
     }
 
     public InstanceInitializerDescription build() {
-      assert parentConstructor != null;
+      assert isValid();
       return new InstanceInitializerDescription(
-          arity, instanceFieldAssignments, parentConstructor, parentConstructorArguments);
+          instanceFieldAssignmentsPre,
+          instanceFieldAssignmentsPost,
+          parentConstructor,
+          parentConstructorArguments,
+          relaxedParameters);
+    }
+
+    public boolean isValid() {
+      return parentConstructor != null;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
index c2063f3..862a23c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -10,11 +10,13 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeUtils;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -24,61 +26,62 @@
 import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
-import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 
 public class InstanceInitializerMerger {
 
-  private final AppView<?> appView;
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Reference2IntMap<DexType> classIdentifiers;
   private final DexItemFactory dexItemFactory;
   private final MergeGroup group;
   private final List<ProgramMethod> instanceInitializers;
   private final InstanceInitializerDescription instanceInitializerDescription;
+  private final HorizontalClassMergerGraphLens.Builder lensBuilder;
   private final Mode mode;
 
   InstanceInitializerMerger(
-      AppView<?> appView, MergeGroup group, List<ProgramMethod> instanceInitializers, Mode mode) {
-    this(appView, group, instanceInitializers, mode, null);
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
+      MergeGroup group,
+      List<ProgramMethod> instanceInitializers,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      Mode mode) {
+    this(appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode, null);
   }
 
   InstanceInitializerMerger(
-      AppView<?> appView,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
       MergeGroup group,
       List<ProgramMethod> instanceInitializers,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode,
       InstanceInitializerDescription instanceInitializerDescription) {
     this.appView = appView;
+    this.classIdentifiers = classIdentifiers;
     this.dexItemFactory = appView.dexItemFactory();
     this.group = group;
     this.instanceInitializers = instanceInitializers;
     this.instanceInitializerDescription = instanceInitializerDescription;
+    this.lensBuilder = lensBuilder;
     this.mode = mode;
 
-    // Constructors should not be empty and all constructors should have the same prototype.
+    // Constructors should not be empty and all constructors should have the same prototype unless
+    // equivalent.
     assert !instanceInitializers.isEmpty();
-    assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1;
-  }
-
-  /**
-   * The method reference template describes which arguments the constructor must have, and is used
-   * to generate the final reference by appending null arguments until it is fresh.
-   */
-  private DexMethod generateReferenceMethodTemplate() {
-    DexMethod methodTemplate = instanceInitializers.iterator().next().getReference();
-    if (instanceInitializers.size() > 1) {
-      methodTemplate = dexItemFactory.appendTypeToMethod(methodTemplate, dexItemFactory.intType);
-    }
-    return methodTemplate.withHolder(group.getTarget(), dexItemFactory);
+    assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1
+        || instanceInitializerDescription != null;
   }
 
   public int getArity() {
@@ -89,6 +92,78 @@
     return instanceInitializers;
   }
 
+  private CfVersion getNewClassFileVersion() {
+    CfVersion classFileVersion = null;
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
+      if (instanceInitializer.getDefinition().hasClassFileVersion()) {
+        classFileVersion =
+            Ordered.maxIgnoreNull(
+                classFileVersion, instanceInitializer.getDefinition().getClassFileVersion());
+      }
+    }
+    return classFileVersion;
+  }
+
+  private DexMethod getNewMethodReference(ProgramMethod representative) {
+    return getNewMethodReference(representative, false);
+  }
+
+  private DexMethod getNewMethodReference(ProgramMethod representative, boolean needsClassId) {
+    DexType[] oldParameters = representative.getParameters().values;
+    DexType[] newParameters =
+        new DexType[representative.getParameters().size() + BooleanUtils.intValue(needsClassId)];
+    System.arraycopy(oldParameters, 0, newParameters, 0, oldParameters.length);
+    for (int i = 0; i < oldParameters.length; i++) {
+      final int parameterIndex = i;
+      newParameters[i] =
+          DexTypeUtils.computeLeastUpperBound(
+              appView,
+              Iterables.transform(
+                  instanceInitializers,
+                  instanceInitializer -> instanceInitializer.getParameter(parameterIndex)));
+    }
+    if (needsClassId) {
+      assert ArrayUtils.last(newParameters) == null;
+      newParameters[newParameters.length - 1] = dexItemFactory.intType;
+    }
+    return dexItemFactory.createInstanceInitializer(group.getTarget().getType(), newParameters);
+  }
+
+  private DexMethod getOriginalMethodReference() {
+    return appView.graphLens().getOriginalMethodSignature(getRepresentative().getReference());
+  }
+
+  private ProgramMethod getRepresentative() {
+    return ListUtils.first(instanceInitializers);
+  }
+
+  /**
+   * Returns a special original method signature for the synthesized constructor that did not exist
+   * prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
+   * constructor corresponds to the previous <init>() method on the target class, which could have
+   * unintended side-effects such as leading to unused argument removal being applied to the
+   * synthesized constructor all-though it by construction doesn't have any unused arguments.
+   */
+  private DexMethod getSyntheticMethodReference(
+      ClassMethodsBuilder classMethodsBuilder, ProgramMethod representative) {
+    return dexItemFactory.createFreshMethodNameWithoutHolder(
+        "$r8$init$synthetic",
+        representative.getProto(),
+        representative.getHolderType(),
+        classMethodsBuilder::isFresh);
+  }
+
+  private Int2ReferenceSortedMap<DexMethod> createClassIdToInstanceInitializerMap() {
+    assert !hasInstanceInitializerDescription();
+    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
+      typeConstructorClassMap.put(
+          classIdentifiers.getInt(instanceInitializer.getHolderType()),
+          lensBuilder.getRenamedMethodSignature(instanceInitializer.getReference()));
+    }
+    return typeConstructorClassMap;
+  }
+
   public int size() {
     return instanceInitializers.size();
   }
@@ -96,12 +171,20 @@
   public static class Builder {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final Reference2IntMap<DexType> classIdentifiers;
     private int estimatedDexCodeSize;
     private final List<List<ProgramMethod>> instanceInitializerGroups = new ArrayList<>();
+    private final HorizontalClassMergerGraphLens.Builder lensBuilder;
     private final Mode mode;
 
-    public Builder(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    public Builder(
+        AppView<? extends AppInfoWithClassHierarchy> appView,
+        Reference2IntMap<DexType> classIdentifiers,
+        HorizontalClassMergerGraphLens.Builder lensBuilder,
+        Mode mode) {
       this.appView = appView;
+      this.classIdentifiers = classIdentifiers;
+      this.lensBuilder = lensBuilder;
       this.mode = mode;
       createNewGroup();
     }
@@ -136,7 +219,8 @@
       return ListUtils.map(
           instanceInitializerGroups,
           instanceInitializers ->
-              new InstanceInitializerMerger(appView, group, instanceInitializers, mode));
+              new InstanceInitializerMerger(
+                  appView, classIdentifiers, group, instanceInitializers, lensBuilder, mode));
     }
 
     public InstanceInitializerMerger buildSingle(
@@ -145,56 +229,18 @@
       assert instanceInitializerGroups.size() == 1;
       List<ProgramMethod> instanceInitializers = ListUtils.first(instanceInitializerGroups);
       return new InstanceInitializerMerger(
-          appView, group, instanceInitializers, mode, instanceInitializerDescription);
+          appView,
+          classIdentifiers,
+          group,
+          instanceInitializers,
+          lensBuilder,
+          mode,
+          instanceInitializerDescription);
     }
   }
 
-  // Returns true if we can simply use an existing constructor as the new constructor.
-  private boolean isTrivialMerge(ClassMethodsBuilder classMethodsBuilder) {
-    if (group.hasClassIdField()) {
-      // We need to set the class id field.
-      return false;
-    }
-    DexMethod trivialInstanceInitializerReference =
-        ListUtils.first(instanceInitializers)
-            .getReference()
-            .withHolder(group.getTarget(), dexItemFactory);
-    if (!classMethodsBuilder.isFresh(trivialInstanceInitializerReference)) {
-      // We need to append null arguments for disambiguation.
-      return false;
-    }
-    return isMergeOfEquivalentInstanceInitializers();
-  }
-
-  private boolean isMergeOfEquivalentInstanceInitializers() {
-    Iterator<ProgramMethod> instanceInitializerIterator = instanceInitializers.iterator();
-    ProgramMethod firstInstanceInitializer = instanceInitializerIterator.next();
-    if (!instanceInitializerIterator.hasNext()) {
-      return true;
-    }
-    // We need all the constructors to be equivalent.
-    InstanceInitializerInfo instanceInitializerInfo =
-        firstInstanceInitializer
-            .getDefinition()
-            .getOptimizationInfo()
-            .getContextInsensitiveInstanceInitializerInfo();
-    if (!instanceInitializerInfo.hasParent()) {
-      // We don't know the parent constructor of the first constructor.
-      return false;
-    }
-    DexMethod parent = instanceInitializerInfo.getParent();
-    return Iterables.all(
-        instanceInitializers,
-        instanceInitializer ->
-            isSideEffectFreeInstanceInitializerWithParent(instanceInitializer, parent));
-  }
-
-  private boolean isSideEffectFreeInstanceInitializerWithParent(
-      ProgramMethod instanceInitializer, DexMethod parent) {
-    MethodOptimizationInfo optimizationInfo =
-        instanceInitializer.getDefinition().getOptimizationInfo();
-    return !optimizationInfo.mayHaveSideEffects()
-        && optimizationInfo.getContextInsensitiveInstanceInitializerInfo().getParent() == parent;
+  private boolean hasInstanceInitializerDescription() {
+    return instanceInitializerDescription != null;
   }
 
   private DexMethod moveInstanceInitializer(
@@ -219,132 +265,118 @@
     return method;
   }
 
-  private MethodAccessFlags getAccessFlags() {
+  private MethodAccessFlags getNewAccessFlags() {
     // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
     return MethodAccessFlags.fromSharedAccessFlags(
         Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true);
   }
 
+  private Code getNewCode(
+      DexMethod newMethodReference,
+      DexMethod syntheticMethodReference,
+      boolean needsClassId,
+      int extraNulls) {
+    if (hasInstanceInitializerDescription()) {
+      if (mode.isInitial() || appView.options().isGeneratingClassFiles()) {
+        return instanceInitializerDescription.createCfCode(
+            newMethodReference,
+            getOriginalMethodReference(),
+            syntheticMethodReference,
+            group,
+            needsClassId,
+            extraNulls);
+      }
+      return instanceInitializerDescription.createDexCode(
+          newMethodReference,
+          getOriginalMethodReference(),
+          syntheticMethodReference,
+          group,
+          needsClassId,
+          extraNulls);
+    }
+    if (isSingleton() && !group.hasClassIdField()) {
+      return getRepresentative().getDefinition().getCode();
+    }
+    return new ConstructorEntryPointSynthesizedCode(
+        createClassIdToInstanceInitializerMap(),
+        newMethodReference,
+        group.hasClassIdField() ? group.getClassIdField() : null,
+        syntheticMethodReference);
+  }
+
+  private boolean isSingleton() {
+    return instanceInitializers.size() == 1;
+  }
+
   /** Synthesize a new method which selects the constructor based on a parameter type. */
   void merge(
       ClassMethodsBuilder classMethodsBuilder,
-      HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Reference2IntMap<DexType> classIdentifiers,
       SyntheticArgumentClass syntheticArgumentClass) {
-    // TODO(b/189296638): Handle merging of equivalent constructors when
-    //  `instanceInitializerDescription` is set.
-    if (isTrivialMerge(classMethodsBuilder)) {
-      mergeTrivial(classMethodsBuilder, lensBuilder);
-      return;
-    }
-
-    assert mode.isInitial();
-
-    // Tree map as must be sorted.
-    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
-
-    // Move constructors to target class.
-    CfVersion classFileVersion = null;
-    for (ProgramMethod instanceInitializer : instanceInitializers) {
-      if (instanceInitializer.getDefinition().hasClassFileVersion()) {
-        classFileVersion =
-            Ordered.maxIgnoreNull(
-                classFileVersion, instanceInitializer.getDefinition().getClassFileVersion());
-      }
-      DexMethod movedInstanceInitializer =
-          moveInstanceInitializer(classMethodsBuilder, instanceInitializer);
-      lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer);
-      lensBuilder.recordNewMethodSignature(
-          instanceInitializer.getReference(), movedInstanceInitializer);
-      typeConstructorClassMap.put(
-          classIdentifiers.getInt(instanceInitializer.getHolderType()), movedInstanceInitializer);
-    }
-
-    // Create merged constructor reference.
-    DexMethod methodReferenceTemplate = generateReferenceMethodTemplate();
-    DexMethod newConstructorReference =
-        dexItemFactory.createInstanceInitializerWithFreshProto(
-            methodReferenceTemplate,
-            syntheticArgumentClass.getArgumentClasses(),
-            classMethodsBuilder::isFresh);
-    int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
-
     ProgramMethod representative = ListUtils.first(instanceInitializers);
-    DexMethod originalConstructorReference =
-        appView.graphLens().getOriginalMethodSignature(representative.getReference());
 
-    // Create a special original method signature for the synthesized constructor that did not exist
-    // prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
-    // constructor corresponds to the previous <init>() method on the target class, which could have
-    // unintended side-effects such as leading to unused argument removal being applied to the
-    // synthesized constructor all-though it by construction doesn't have any unused arguments.
-    DexMethod bridgeConstructorReference =
-        dexItemFactory.createFreshMethodNameWithoutHolder(
-            "$r8$init$bridge",
-            originalConstructorReference.getProto(),
-            originalConstructorReference.getHolderType(),
+    // Create merged instance initializer reference.
+    boolean needsClassId =
+        instanceInitializers.size() > 1
+            && (!hasInstanceInitializerDescription() || group.hasClassIdField());
+    assert mode.isInitial() || !needsClassId;
+
+    DexMethod newMethodReferenceTemplate = getNewMethodReference(representative, needsClassId);
+    assert mode.isInitial() || classMethodsBuilder.isFresh(newMethodReferenceTemplate);
+
+    DexMethod newMethodReference =
+        dexItemFactory.createInstanceInitializerWithFreshProto(
+            newMethodReferenceTemplate,
+            mode.isInitial() ? syntheticArgumentClass.getArgumentClasses() : ImmutableList.of(),
             classMethodsBuilder::isFresh);
+    int extraNulls = newMethodReference.getArity() - newMethodReferenceTemplate.getArity();
 
-    ConstructorEntryPointSynthesizedCode synthesizedCode =
-        new ConstructorEntryPointSynthesizedCode(
-            typeConstructorClassMap,
-            newConstructorReference,
-            group.hasClassIdField() ? group.getClassIdField() : null,
-            bridgeConstructorReference);
-    DexEncodedMethod newConstructor =
-        new DexEncodedMethod(
-            newConstructorReference,
-            getAccessFlags(),
-            MethodTypeSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            ParameterAnnotationsList.empty(),
-            synthesizedCode,
-            true,
-            classFileVersion);
+    // Verify that the merge is a simple renaming in the final round of merging.
+    assert mode.isInitial() || newMethodReference == newMethodReferenceTemplate;
 
-    // Map each old constructor to the newly synthesized constructor in the graph lens.
-    for (ProgramMethod oldInstanceInitializer : instanceInitializers) {
+    // Move instance initializers to target class.
+    if (hasInstanceInitializerDescription()) {
+      lensBuilder.moveMethods(instanceInitializers, newMethodReference);
+    } else if (isSingleton() && !group.hasClassIdField()) {
+      lensBuilder.moveMethod(representative.getReference(), newMethodReference, true);
+    } else {
+      for (ProgramMethod instanceInitializer : instanceInitializers) {
+        DexMethod movedInstanceInitializer =
+            moveInstanceInitializer(classMethodsBuilder, instanceInitializer);
+        lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer);
+        lensBuilder.recordNewMethodSignature(
+            instanceInitializer.getReference(), movedInstanceInitializer);
+      }
+    }
+
+    // Add a mapping from a synthetic name to the synthetic constructor.
+    DexMethod syntheticMethodReference =
+        getSyntheticMethodReference(classMethodsBuilder, representative);
+    if (!isSingleton() || group.hasClassIdField()) {
+      lensBuilder.recordNewMethodSignature(syntheticMethodReference, newMethodReference, true);
+    }
+
+    // Map each of the instance initializers to the new instance initializer in the graph lens.
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
       List<ExtraParameter> extraParameters = new ArrayList<>();
-      if (instanceInitializers.size() > 1) {
-        int classIdentifier = classIdentifiers.getInt(oldInstanceInitializer.getHolderType());
+      if (needsClassId) {
+        int classIdentifier = classIdentifiers.getInt(instanceInitializer.getHolderType());
         extraParameters.add(new ExtraConstantIntParameter(classIdentifier));
       }
       extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
       lensBuilder.mapMergedConstructor(
-          oldInstanceInitializer.getReference(), newConstructorReference, extraParameters);
+          instanceInitializer.getReference(), newMethodReference, extraParameters);
     }
 
-    // Add a mapping from a synthetic name to the synthetic constructor.
-    lensBuilder.recordNewMethodSignature(bridgeConstructorReference, newConstructorReference);
-
-    classMethodsBuilder.addDirectMethod(newConstructor);
-  }
-
-  private void mergeTrivial(
-      ClassMethodsBuilder classMethodsBuilder, HorizontalClassMergerGraphLens.Builder lensBuilder) {
-    ProgramMethod representative = ListUtils.first(instanceInitializers);
-    DexMethod newMethodReference =
-        representative.getReference().withHolder(group.getTarget(), dexItemFactory);
-
-    for (ProgramMethod constructor : instanceInitializers) {
-      boolean isRepresentative = constructor == representative;
-      lensBuilder.moveMethod(constructor.getReference(), newMethodReference, isRepresentative);
-    }
-
-    DexEncodedMethod newMethod =
-        representative.getHolder() == group.getTarget()
-            ? representative.getDefinition()
-            : representative.getDefinition().toTypeSubstitutedMethod(newMethodReference);
-    fixupAccessFlagsForTrivialMerge(newMethod.getAccessFlags());
-
-    classMethodsBuilder.addDirectMethod(newMethod);
-  }
-
-  private void fixupAccessFlagsForTrivialMerge(MethodAccessFlags accessFlags) {
-    if (!accessFlags.isPublic()) {
-      accessFlags.unsetPrivate();
-      accessFlags.unsetProtected();
-      accessFlags.setPublic();
-    }
+    DexEncodedMethod newInstanceInitializer =
+        new DexEncodedMethod(
+            newMethodReference,
+            getNewAccessFlags(),
+            MethodTypeSignature.noSignature(),
+            DexAnnotationSet.empty(),
+            ParameterAnnotationsList.empty(),
+            getNewCode(newMethodReference, syntheticMethodReference, needsClassId, extraNulls),
+            true,
+            getNewClassFileVersion());
+    classMethodsBuilder.addDirectMethod(newInstanceInitializer);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
index ed7e099..b5e485b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMergerCollection.java
@@ -10,9 +10,11 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.InstanceInitializerMerger.Builder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
@@ -30,14 +32,15 @@
       List<InstanceInitializerMerger> instanceInitializerMergers,
       Map<InstanceInitializerDescription, InstanceInitializerMerger>
           equivalentInstanceInitializerMergers) {
-    assert equivalentInstanceInitializerMergers.isEmpty();
     this.instanceInitializerMergers = instanceInitializerMergers;
     this.equivalentInstanceInitializerMergers = equivalentInstanceInitializerMergers;
   }
 
   public static InstanceInitializerMergerCollection create(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      Reference2IntMap<DexType> classIdentifiers,
       MergeGroup group,
+      ClassInstanceFieldsMerger instanceFieldsMerger,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Mode mode) {
     // Create an instance initializer merger for each group of instance initializers that are
@@ -50,12 +53,16 @@
                 DexEncodedMethod::isInstanceInitializer,
                 instanceInitializer -> {
                   InstanceInitializerDescription description =
-                      InstanceInitializerAnalysis.analyze(instanceInitializer, lensBuilder);
+                      InstanceInitializerAnalysis.analyze(
+                          appView, group, instanceInitializer, instanceFieldsMerger, mode);
                   if (description != null) {
                     buildersByDescription
                         .computeIfAbsent(
                             description,
-                            ignoreKey(() -> new InstanceInitializerMerger.Builder(appView, mode)))
+                            ignoreKey(
+                                () ->
+                                    new InstanceInitializerMerger.Builder(
+                                        appView, classIdentifiers, lensBuilder, mode)))
                         .addEquivalent(instanceInitializer);
                   } else {
                     buildersWithoutDescription.add(instanceInitializer);
@@ -86,7 +93,9 @@
               buildersByProto
                   .computeIfAbsent(
                       instanceInitializer.getDefinition().getProto(),
-                      ignore -> new InstanceInitializerMerger.Builder(appView, mode))
+                      ignore ->
+                          new InstanceInitializerMerger.Builder(
+                              appView, classIdentifiers, lensBuilder, mode))
                   .add(instanceInitializer));
       for (InstanceInitializerMerger.Builder builder : buildersByProto.values()) {
         instanceInitializerMergers.addAll(builder.build(group));
@@ -95,7 +104,8 @@
       buildersWithoutDescription.forEach(
           instanceInitializer ->
               instanceInitializerMergers.addAll(
-                  new InstanceInitializerMerger.Builder(appView, mode)
+                  new InstanceInitializerMerger.Builder(
+                          appView, classIdentifiers, lensBuilder, mode)
                       .add(instanceInitializer)
                       .build(group)));
     }
@@ -110,5 +120,6 @@
 
   public void forEach(Consumer<InstanceInitializerMerger> consumer) {
     instanceInitializerMergers.forEach(consumer);
+    equivalentInstanceInitializerMergers.values().forEach(consumer);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index 11fa11e..ade97ad 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -152,13 +152,10 @@
       for (CfInstruction instruction : code.getInstructions()) {
         if (instruction.isPosition()) {
           CfPosition cfPosition = instruction.asPosition();
-          Position position = cfPosition.getPosition();
           newInstructions.add(
               new CfPosition(
                   cfPosition.getLabel(),
-                  position.hasCallerPosition()
-                      ? position
-                      : position.withCallerPosition(callerPosition)));
+                  cfPosition.getPosition().withOutermostCallerPosition(callerPosition)));
         } else if (instruction.isReturn()) {
           if (code.getInstructions().size() != index) {
             newInstructions.add(new CfGoto(endLabel));
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
index bbc5e39..0ff172c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -86,6 +86,11 @@
     InstanceInitializerInfo otherInstanceInitializerInfo =
         other.getDefinition().getOptimizationInfo().getContextInsensitiveInstanceInitializerInfo();
     assert otherInstanceInitializerInfo.isNonTrivialInstanceInitializerInfo();
+    if (!instanceInitializerInfo.hasParent()
+        || instanceInitializerInfo.getParent().getArity() > 0) {
+      return false;
+    }
+
     if (instanceInitializerInfo.getParent() != otherInstanceInitializerInfo.getParent()) {
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
index f2d8962..13df457 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleDexItemBasedStringValue.java
@@ -34,6 +34,14 @@
     this.nameComputationInfo = nameComputationInfo;
   }
 
+  public DexReference getItem() {
+    return item;
+  }
+
+  public NameComputationInfo<?> getNameComputationInfo() {
+    return nameComputationInfo;
+  }
+
   @Override
   public boolean isSingleDexItemBasedStringValue() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Position.java b/src/main/java/com/android/tools/r8/ir/code/Position.java
index 6cba1ee..09a2631 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Position.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Position.java
@@ -137,10 +137,21 @@
     return lastPosition;
   }
 
+  public Position getCallerPosition() {
+    return callerPosition;
+  }
+
   public Position withCallerPosition(Position callerPosition) {
     return new Position(line, file, method, callerPosition, synthetic);
   }
 
+  public Position withOutermostCallerPosition(Position newOutermostCallerPosition) {
+    return withCallerPosition(
+        hasCallerPosition()
+            ? getCallerPosition().withOutermostCallerPosition(newOutermostCallerPosition)
+            : newOutermostCallerPosition);
+  }
+
   @Override
   public boolean equals(Object other) {
     return other instanceof Position && compareTo((Position) other) == 0;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
index 8f83dca..83e0ff5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
@@ -107,6 +107,9 @@
         assert instructionDesugaringEventConsumer.verifyNothingToFinalize();
       }
 
+      converter.finalizeDesugaredLibraryRetargeting(instructionDesugaringEventConsumer);
+      assert instructionDesugaringEventConsumer.verifyNothingToFinalize();
+
       classes = deferred;
     }
   }
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 fea728d..8ae52ef 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
@@ -222,10 +222,7 @@
       assert options.desugarState.isOn();
       this.instructionDesugaring = CfInstructionDesugaringCollection.create(appView);
       this.classDesugaring = instructionDesugaring.createClassDesugaringCollection();
-      this.desugaredLibraryRetargeter =
-          options.desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()
-              ? null
-              : new DesugaredLibraryRetargeter(appView);
+      this.desugaredLibraryRetargeter = null; // Managed cf to cf.
       this.interfaceMethodRewriter =
           options.desugaredLibraryConfiguration.getEmulateLibraryInterface().isEmpty()
               ? null
@@ -258,14 +255,15 @@
             ? CfInstructionDesugaringCollection.empty()
             : CfInstructionDesugaringCollection.create(appView);
     this.classDesugaring = instructionDesugaring.createClassDesugaringCollection();
+    this.desugaredLibraryRetargeter =
+        options.desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()
+                || !appView.enableWholeProgramOptimizations()
+            ? null
+            : new DesugaredLibraryRetargeter(appView);
     this.interfaceMethodRewriter =
         options.isInterfaceMethodDesugaringEnabled()
             ? new InterfaceMethodRewriter(appView, this)
             : null;
-    this.desugaredLibraryRetargeter =
-        options.desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()
-            ? null
-            : new DesugaredLibraryRetargeter(appView);
     this.covariantReturnTypeAnnotationTransformer =
         options.processCovariantReturnTypeAnnotations
             ? new CovariantReturnTypeAnnotationTransformer(this, appView.dexItemFactory())
@@ -371,6 +369,12 @@
         D8NestBasedAccessDesugaring::clearNestAttributes);
   }
 
+  public void finalizeDesugaredLibraryRetargeting(
+      D8CfInstructionDesugaringEventConsumer instructionDesugaringEventConsumer) {
+    instructionDesugaring.withDesugaredLibraryRetargeter(
+        retargeter -> retargeter.finalizeDesugaring(instructionDesugaringEventConsumer));
+  }
+
   private void staticizeClasses(
       OptimizationFeedback feedback, ExecutorService executorService, GraphLens applied)
       throws ExecutionException {
@@ -397,13 +401,13 @@
     }
   }
 
-  private void synthesizeRetargetClass(Builder<?> builder, ExecutorService executorService)
-      throws ExecutionException {
+  private void synthesizeRetargetClass(ExecutorService executorService) throws ExecutionException {
     if (desugaredLibraryRetargeter != null) {
-      desugaredLibraryRetargeter.synthesizeRetargetClasses(builder, executorService, this);
+      desugaredLibraryRetargeter.synthesizeRetargetClasses(this, executorService);
     }
   }
 
+
   private void synthesizeEnumUnboxingUtilityMethods(ExecutorService executorService)
       throws ExecutionException {
     if (enumUnboxer != null) {
@@ -442,7 +446,6 @@
     Builder<?> builder = application.builder().setHighestSortingString(highestSortingString);
 
     desugarInterfaceMethods(builder, ExcludeDexResources, executor);
-    synthesizeRetargetClass(builder, executor);
     processCovariantReturnTypeAnnotations(builder);
     generateDesugaredLibraryAPIWrappers(builder, executor);
 
@@ -588,8 +591,6 @@
           new NeedsIRDesugarUseRegistry(
               method,
               appView,
-              desugaredLibraryRetargeter,
-              interfaceMethodRewriter,
               desugaredLibraryAPIConverter);
       method.registerCodeReferences(useRegistry);
       return useRegistry.needsDesugaring();
@@ -780,7 +781,7 @@
     feedback.updateVisibleOptimizationInfo();
 
     printPhase("Utility classes synthesis");
-    synthesizeRetargetClass(builder, executorService);
+    synthesizeRetargetClass(executorService);
     synthesizeEnumUnboxingUtilityMethods(executorService);
 
     printPhase("Desugared library API Conversion finalization");
@@ -898,11 +899,15 @@
   }
 
   private void computeReachabilitySensitivity(DexApplication application) {
-    application.classes().forEach(c -> {
-      if (c.hasReachabilitySensitiveAnnotation(options.itemFactory)) {
-        c.methods().forEach(m -> m.getMutableOptimizationInfo().setReachabilitySensitive(true));
-      }
-    });
+    application
+        .classes()
+        .forEach(
+            c -> {
+              if (c.hasReachabilitySensitiveAnnotation(options.itemFactory)) {
+                c.methods()
+                    .forEach(m -> m.getMutableOptimizationInfo().setReachabilitySensitive(true));
+              }
+            });
   }
 
   private void forEachSelectedOutliningMethod(
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
index bbc0bcb..cb8eeab 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
@@ -4,12 +4,6 @@
 
 package com.android.tools.r8.ir.conversion;
 
-import static com.android.tools.r8.ir.code.Invoke.Type.DIRECT;
-import static com.android.tools.r8.ir.code.Invoke.Type.INTERFACE;
-import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
-import static com.android.tools.r8.ir.code.Invoke.Type.SUPER;
-import static com.android.tools.r8.ir.code.Invoke.Type.VIRTUAL;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
@@ -17,29 +11,20 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
-import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter;
-import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 
 class NeedsIRDesugarUseRegistry extends UseRegistry {
 
   private boolean needsDesugaring = false;
   private final ProgramMethod context;
-  private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
-  private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final DesugaredLibraryAPIConverter desugaredLibraryAPIConverter;
 
   public NeedsIRDesugarUseRegistry(
       ProgramMethod method,
       AppView<?> appView,
-      DesugaredLibraryRetargeter desugaredLibraryRetargeter,
-      InterfaceMethodRewriter interfaceMethodRewriter,
       DesugaredLibraryAPIConverter desugaredLibraryAPIConverter) {
     super(appView.dexItemFactory());
     this.context = method;
-    this.desugaredLibraryRetargeter = desugaredLibraryRetargeter;
-    this.interfaceMethodRewriter = interfaceMethodRewriter;
     this.desugaredLibraryAPIConverter = desugaredLibraryAPIConverter;
   }
 
@@ -58,26 +43,14 @@
 
   @Override
   public void registerInvokeVirtual(DexMethod method) {
-    registerLibraryRetargeting(method, false);
-    registerInterfaceMethodRewriting(method, VIRTUAL);
     registerDesugaredLibraryAPIConverter(method);
   }
 
   @Override
   public void registerInvokeDirect(DexMethod method) {
-    registerLibraryRetargeting(method, false);
-    registerInterfaceMethodRewriting(method, DIRECT);
     registerDesugaredLibraryAPIConverter(method);
   }
 
-  private void registerInterfaceMethodRewriting(DexMethod method, Type invokeType) {
-    if (!needsDesugaring) {
-      needsDesugaring =
-          interfaceMethodRewriter != null
-              && interfaceMethodRewriter.needsRewriting(method, invokeType, context);
-    }
-  }
-
   private void registerDesugaredLibraryAPIConverter(DexMethod method) {
     if (!needsDesugaring) {
       needsDesugaring =
@@ -86,33 +59,18 @@
     }
   }
 
-  private void registerLibraryRetargeting(DexMethod method, boolean b) {
-    if (!needsDesugaring) {
-      needsDesugaring =
-          desugaredLibraryRetargeter != null
-              && desugaredLibraryRetargeter.getRetargetedMethod(method, b) != null;
-    }
-  }
-
   @Override
   public void registerInvokeStatic(DexMethod method) {
-    registerLibraryRetargeting(method, false);
-    registerInterfaceMethodRewriting(method, STATIC);
     registerDesugaredLibraryAPIConverter(method);
   }
 
   @Override
   public void registerInvokeInterface(DexMethod method) {
-    registerLibraryRetargeting(method, true);
-    registerInterfaceMethodRewriting(method, INTERFACE);
     registerDesugaredLibraryAPIConverter(method);
   }
 
   @Override
   public void registerInvokeStatic(DexMethod method, boolean itf) {
-    if (itf) {
-      needsDesugaring = true;
-    }
     registerInvokeStatic(method);
   }
 
@@ -124,8 +82,6 @@
 
   @Override
   public void registerInvokeSuper(DexMethod method) {
-    registerLibraryRetargeting(method, false);
-    registerInterfaceMethodRewriting(method, SUPER);
     registerDesugaredLibraryAPIConverter(method);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index 4623428..21608cf 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
@@ -168,13 +169,15 @@
     super.prepareForWaveExtensionProcessing();
   }
 
-  void forEachWaveWithExtension(OptimizationFeedback feedback, ExecutorService executorService)
+  void forEachWaveWithExtension(
+      OptimizationFeedbackDelayed feedback, ExecutorService executorService)
       throws ExecutionException {
     while (!waves.isEmpty()) {
       wave = waves.removeFirst();
       assert !wave.isEmpty();
       assert waveExtension.isEmpty();
       do {
+        assert feedback.noUpdatesLeft();
         ThreadUtils.processItems(
             wave,
             method -> {
@@ -184,6 +187,7 @@
               forEachMethod(method, codeOptimizations, feedback);
             },
             executorService);
+        feedback.updateVisibleOptimizationInfo();
         processed.addAll(wave);
         prepareForWaveExtensionProcessing();
       } while (!wave.isEmpty());
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
index 3fb9329..78291d7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringCollection.java
@@ -56,5 +56,8 @@
   public abstract <T extends Throwable> void withD8NestBasedAccessDesugaring(
       ThrowingConsumer<D8NestBasedAccessDesugaring, T> consumer) throws T;
 
+  public abstract void withDesugaredLibraryRetargeter(
+      Consumer<DesugaredLibraryRetargeter> consumer);
+
   public abstract void withRecordRewriter(Consumer<RecordRewriter> consumer);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 6951b39..1c6b337 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.ProgramField;
@@ -42,7 +43,8 @@
         NestBasedAccessDesugaringEventConsumer,
         RecordDesugaringEventConsumer,
         TwrCloseResourceDesugaringEventConsumer,
-        InterfaceMethodDesugaringEventConsumer {
+        InterfaceMethodDesugaringEventConsumer,
+        DesugaredLibraryRetargeterEventConsumer {
 
   public static D8CfInstructionDesugaringEventConsumer createForD8(
       D8MethodProcessor methodProcessor) {
@@ -61,6 +63,21 @@
     return new CfInstructionDesugaringEventConsumer() {
 
       @Override
+      public void acceptDesugaredLibraryRetargeterDispatchProgramClass(DexProgramClass clazz) {
+        assert false;
+      }
+
+      @Override
+      public void acceptDesugaredLibraryRetargeterDispatchClasspathClass(DexClasspathClass clazz) {
+        assert false;
+      }
+
+      @Override
+      public void acceptForwardingMethod(ProgramMethod method) {
+        assert false;
+      }
+
+      @Override
       public void acceptThrowMethod(ProgramMethod method, ProgramMethod context) {
         assert false;
       }
@@ -132,6 +149,21 @@
     }
 
     @Override
+    public void acceptDesugaredLibraryRetargeterDispatchProgramClass(DexProgramClass clazz) {
+      methodProcessor.scheduleDesugaredMethodsForProcessing(clazz.programMethods());
+    }
+
+    @Override
+    public void acceptDesugaredLibraryRetargeterDispatchClasspathClass(DexClasspathClass clazz) {
+      // Intentionnaly empty.
+    }
+
+    @Override
+    public void acceptForwardingMethod(ProgramMethod method) {
+      methodProcessor.scheduleDesugaredMethodForProcessing(method);
+    }
+
+    @Override
     public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
       methodProcessor.scheduleMethodForProcessing(backportedMethod, this);
     }
@@ -270,6 +302,24 @@
     }
 
     @Override
+    public void acceptDesugaredLibraryRetargeterDispatchProgramClass(DexProgramClass clazz) {
+      // Called only in Desugared library compilation which is D8.
+      assert false;
+    }
+
+    @Override
+    public void acceptDesugaredLibraryRetargeterDispatchClasspathClass(DexClasspathClass clazz) {
+      // TODO(b/188767735): R8 currently relies on IR desugaring.
+      // The classpath class should be marked as liveNonProgramType.
+    }
+
+    @Override
+    public void acceptForwardingMethod(ProgramMethod method) {
+      // TODO(b/188767735): R8 currently relies on IR desugaring.
+      // The method should be marked live, and assert everything it references is traced.
+    }
+
+    @Override
     public void acceptRecordClass(DexProgramClass recordClass) {
       // This is called each time an instruction or a class is found to require the record class.
       assert false : "TODO(b/179146128): To be implemented";
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 9ec7242..a069091 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
@@ -153,6 +153,17 @@
     return synthesizedLibraryClassesPackagePrefix;
   }
 
+  // TODO(b/183918843): We are currently computing a new name for the class by replacing the
+  //  initial package prefix by the synthesized library class package prefix, it would be better
+  //  to make the rewriting explicit in the desugared library json file.
+  public String convertJavaNameToDesugaredLibrary(DexType type) {
+    String prefix =
+        DescriptorUtils.getJavaTypeFromBinaryName(getSynthesizedLibraryClassesPackagePrefix());
+    String interfaceType = type.toString();
+    int firstPackage = interfaceType.indexOf('.');
+    return prefix + interfaceType.substring(firstPackage + 1);
+  }
+
   public String getIdentifier() {
     return identifier;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
index 5a96760..a0153a0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
@@ -4,15 +4,18 @@
 
 package com.android.tools.r8.ir.desugar;
 
+import static com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter.InvokeRetargetingResult.NO_REWRITING;
+
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.dex.Constants;
 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.ClassAccessFlags;
+import com.android.tools.r8.graph.ClasspathOrLibraryClass;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -21,7 +24,6 @@
 import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProgramClass.ChecksumSupplier;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -36,6 +38,7 @@
 import com.android.tools.r8.graph.NestHostClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -45,13 +48,16 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.synthetic.EmulateInterfaceSyntheticCfCodeProvider;
 import com.android.tools.r8.origin.SynthesizedOrigin;
-import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.synthesis.SyntheticClassBuilder;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.collect.Maps;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.IdentityHashMap;
@@ -64,12 +70,10 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
+import java.util.function.Function;
+import org.objectweb.asm.Opcodes;
 
-public class DesugaredLibraryRetargeter {
-
-  private static final String RETARGET_PACKAGE = "retarget/";
-  public static final String DESUGAR_LIB_RETARGET_CLASS_NAME_PREFIX =
-      "$r8$retargetLibraryMember$virtualDispatch";
+public class DesugaredLibraryRetargeter implements CfInstructionDesugaring {
 
   private final AppView<?> appView;
   private final Map<DexMethod, DexMethod> retargetLibraryMember = new IdentityHashMap<>();
@@ -80,27 +84,16 @@
   // Non final virtual library methods requiring generation of emulated dispatch.
   private final DexClassAndMethodSet emulatedDispatchMethods = DexClassAndMethodSet.create();
 
-  private final String packageAndClassDescriptorPrefix;
+  private final SortedProgramMethodSet forwardingMethods = SortedProgramMethodSet.create();
 
   public DesugaredLibraryRetargeter(AppView<?> appView) {
     this.appView = appView;
-    packageAndClassDescriptorPrefix =
-        getRetargetPackageAndClassPrefixDescriptor(appView.options().desugaredLibraryConfiguration);
     if (appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()) {
       return;
     }
     new RetargetingSetup().setUpRetargeting();
   }
 
-  public static boolean isRetargetType(DexType type, InternalOptions options) {
-    if (options.desugaredLibraryConfiguration == null) {
-      return false;
-    }
-    return type.toDescriptorString()
-        .startsWith(
-            getRetargetPackageAndClassPrefixDescriptor(options.desugaredLibraryConfiguration));
-  }
-
   public static void checkForAssumedLibraryTypes(AppView<?> appView) {
     Map<DexString, Map<DexType, DexType>> retargetCoreLibMember =
         appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember();
@@ -225,55 +218,37 @@
     appView.options().reporter.warning(warning);
   }
 
-  private static void synthesizeClassWithUniqueMethod(
-      Builder<?> builder,
-      ClassAccessFlags accessFlags,
-      DexType type,
-      DexEncodedMethod uniqueMethod,
-      String origin,
-      AppView<?> appView) {
-    DexItemFactory factory = appView.dexItemFactory();
-    DexProgramClass newClass =
-        new DexProgramClass(
-            type,
-            null,
-            new SynthesizedOrigin(origin, BackportedMethodRewriter.class),
-            accessFlags,
-            factory.objectType,
-            DexTypeList.empty(),
-            null,
-            null,
-            Collections.emptyList(),
-            null,
-            Collections.emptyList(),
-            ClassSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            DexEncodedField.EMPTY_ARRAY,
-            DexEncodedField.EMPTY_ARRAY,
-            uniqueMethod.isStatic()
-                ? new DexEncodedMethod[] {uniqueMethod}
-                : DexEncodedMethod.EMPTY_ARRAY,
-            uniqueMethod.isStatic()
-                ? DexEncodedMethod.EMPTY_ARRAY
-                : new DexEncodedMethod[] {uniqueMethod},
-            factory.getSkipNameValidationForTesting(),
-            getChecksumSupplier(uniqueMethod, appView));
-    appView.appInfo().addSynthesizedClassForLibraryDesugaring(newClass);
-    builder.addSynthesizedClass(newClass);
-  }
-
-  private static ChecksumSupplier getChecksumSupplier(DexEncodedMethod method, AppView<?> appView) {
-    if (!appView.options().encodeChecksums) {
-      return DexProgramClass::invalidChecksumRequest;
-    }
-    return c -> method.getReference().hashCode();
-  }
-
   // Used by the ListOfBackportedMethods utility.
   void visit(Consumer<DexMethod> consumer) {
     retargetLibraryMember.keySet().forEach(consumer);
   }
 
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      DexItemFactory dexItemFactory) {
+    InvokeRetargetingResult invokeRetargetingResult = computeNewInvokeTarget(instruction, context);
+
+    if (!invokeRetargetingResult.hasNewInvokeTarget()) {
+      return null;
+    }
+
+    DexMethod newInvokeTarget = invokeRetargetingResult.getNewInvokeTarget(eventConsumer);
+    return Collections.singletonList(
+        new CfInvoke(Opcodes.INVOKESTATIC, newInvokeTarget, instruction.asInvoke().isInterface()));
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    return computeNewInvokeTarget(instruction, context).hasNewInvokeTarget();
+  }
+
+  @Deprecated // Use Cf to Cf desugaring instead.
   public void desugar(IRCode code) {
     if (retargetLibraryMember.isEmpty()) {
       return;
@@ -290,62 +265,129 @@
       DexMethod invokedMethod = invoke.getInvokedMethod();
       boolean isInterface = invoke.getInterfaceBit();
 
-      DexMethod retarget = getRetargetedMethod(invokedMethod, isInterface);
-      if (retarget == null) {
-        continue;
+      InvokeRetargetingResult invokeRetargetingResult =
+          computeNewInvokeTarget(
+              invokedMethod, isInterface, invoke.isInvokeSuper(), code.context());
+      if (invokeRetargetingResult.hasNewInvokeTarget()) {
+        DexMethod newInvokeTarget = invokeRetargetingResult.getNewInvokeTarget(null);
+        iterator.replaceCurrentInstruction(
+            new InvokeStatic(newInvokeTarget, invoke.outValue(), invoke.inValues()));
       }
-
-      // Due to emulated dispatch, we have to rewrite invoke-super differently or we end up in
-      // infinite loops. We do direct resolution. This is a very uncommon case.
-      if (invoke.isInvokeSuper() && matchesNonFinalHolderRewrite(invoke.getInvokedMethod())) {
-        DexClassAndMethod superTarget =
-            appView
-                .appInfoForDesugaring()
-                .lookupSuperTarget(invoke.getInvokedMethod(), code.context());
-        // Final methods can be rewritten as a normal invoke.
-        if (superTarget != null && !superTarget.getAccessFlags().isFinal()) {
-          DexMethod retargetMethod =
-              appView.options().desugaredLibraryConfiguration.retargetMethod(superTarget, appView);
-          if (retargetMethod != null) {
-            iterator.replaceCurrentInstruction(
-                new InvokeStatic(retargetMethod, invoke.outValue(), invoke.arguments()));
-          }
-          continue;
-        }
-      }
-
-      iterator.replaceCurrentInstruction(
-          new InvokeStatic(retarget, invoke.outValue(), invoke.inValues()));
     }
   }
 
-  public DexMethod getRetargetedMethod(DexMethod invokedMethod, boolean isInterface) {
-    DexMethod retarget = getRetargetLibraryMember(invokedMethod);
-    if (retarget == null) {
+  static class InvokeRetargetingResult {
+
+    static InvokeRetargetingResult NO_REWRITING =
+        new InvokeRetargetingResult(false, ignored -> null);
+
+    private final boolean hasNewInvokeTarget;
+    private final Function<DesugaredLibraryRetargeterEventConsumer, DexMethod>
+        newInvokeTargetSupplier;
+
+    static InvokeRetargetingResult createInvokeRetargetingResult(DexMethod retarget) {
+      if (retarget == null) {
+        return NO_REWRITING;
+      }
+      return new InvokeRetargetingResult(true, ignored -> retarget);
+    }
+
+    private InvokeRetargetingResult(
+        boolean hasNewInvokeTarget,
+        Function<DesugaredLibraryRetargeterEventConsumer, DexMethod> newInvokeTargetSupplier) {
+      this.hasNewInvokeTarget = hasNewInvokeTarget;
+      this.newInvokeTargetSupplier = newInvokeTargetSupplier;
+    }
+
+    public boolean hasNewInvokeTarget() {
+      return hasNewInvokeTarget;
+    }
+
+    public DexMethod getNewInvokeTarget(DesugaredLibraryRetargeterEventConsumer eventConsumer) {
+      assert hasNewInvokeTarget();
+      return newInvokeTargetSupplier.apply(eventConsumer);
+    }
+  }
+
+  public boolean hasNewInvokeTarget(
+      DexMethod invokedMethod, boolean isInterface, boolean isInvokeSuper, ProgramMethod context) {
+    return computeNewInvokeTarget(invokedMethod, isInterface, isInvokeSuper, context)
+        .hasNewInvokeTarget();
+  }
+
+  private InvokeRetargetingResult computeNewInvokeTarget(
+      CfInstruction instruction, ProgramMethod context) {
+    if (retargetLibraryMember.isEmpty() || !instruction.isInvoke()) {
+      return NO_REWRITING;
+    }
+    CfInvoke cfInvoke = instruction.asInvoke();
+    return computeNewInvokeTarget(
+        cfInvoke.getMethod(),
+        cfInvoke.isInterface(),
+        cfInvoke.isInvokeSuper(context.getHolderType()),
+        context);
+  }
+
+  private InvokeRetargetingResult computeNewInvokeTarget(
+      DexMethod invokedMethod, boolean isInterface, boolean isInvokeSuper, ProgramMethod context) {
+    InvokeRetargetingResult retarget = computeRetargetedMethod(invokedMethod, isInterface);
+    if (!retarget.hasNewInvokeTarget()) {
+      return NO_REWRITING;
+    }
+    if (isInvokeSuper && matchesNonFinalHolderRewrite(invokedMethod)) {
+      DexClassAndMethod superTarget =
+          appView.appInfoForDesugaring().lookupSuperTarget(invokedMethod, context);
+      // Final methods can be rewritten as a normal invoke.
+      if (superTarget != null && !superTarget.getAccessFlags().isFinal()) {
+        return InvokeRetargetingResult.createInvokeRetargetingResult(
+            appView.options().desugaredLibraryConfiguration.retargetMethod(superTarget, appView));
+      }
+    }
+    return retarget;
+  }
+
+  private InvokeRetargetingResult computeRetargetedMethod(
+      DexMethod invokedMethod, boolean isInterface) {
+    InvokeRetargetingResult invokeRetargetingResult = computeRetargetLibraryMember(invokedMethod);
+    if (!invokeRetargetingResult.hasNewInvokeTarget()) {
       if (!matchesNonFinalHolderRewrite(invokedMethod)) {
-        return null;
+        return NO_REWRITING;
       }
       // We need to force resolution, even on d8, to know if the invoke has to be rewritten.
       ResolutionResult resolutionResult =
           appView.appInfoForDesugaring().resolveMethod(invokedMethod, isInterface);
       if (resolutionResult.isFailedResolution()) {
-        return null;
+        return NO_REWRITING;
       }
       DexEncodedMethod singleTarget = resolutionResult.getSingleTarget();
       assert singleTarget != null;
-      retarget = getRetargetLibraryMember(singleTarget.getReference());
+      invokeRetargetingResult = computeRetargetLibraryMember(singleTarget.getReference());
     }
-    return retarget;
+    return invokeRetargetingResult;
   }
 
-  private DexMethod getRetargetLibraryMember(DexMethod method) {
+  private InvokeRetargetingResult computeRetargetLibraryMember(DexMethod method) {
     Map<DexType, DexType> backportCoreLibraryMembers =
         appView.options().desugaredLibraryConfiguration.getBackportCoreLibraryMember();
     if (backportCoreLibraryMembers.containsKey(method.holder)) {
       DexType newHolder = backportCoreLibraryMembers.get(method.holder);
-      return appView.dexItemFactory().createMethod(newHolder, method.proto, method.name);
+      DexMethod newMethod =
+          appView.dexItemFactory().createMethod(newHolder, method.proto, method.name);
+      return InvokeRetargetingResult.createInvokeRetargetingResult(newMethod);
     }
-    return retargetLibraryMember.get(method);
+    DexClassAndMethod emulatedMethod = emulatedDispatchMethods.get(method);
+    if (emulatedMethod != null) {
+      assert !emulatedMethod.getAccessFlags().isStatic();
+      return new InvokeRetargetingResult(
+          true,
+          eventConsumer -> {
+            DexType newHolder =
+                ensureEmulatedHolderDispatchMethod(emulatedMethod, eventConsumer).type;
+            return computeRetargetMethod(
+                method, emulatedMethod.getAccessFlags().isStatic(), newHolder);
+          });
+    }
+    return InvokeRetargetingResult.createInvokeRetargetingResult(retargetLibraryMember.get(method));
   }
 
   private boolean matchesNonFinalHolderRewrite(DexMethod method) {
@@ -361,6 +403,12 @@
     return false;
   }
 
+  DexMethod computeRetargetMethod(DexMethod method, boolean isStatic, DexType newHolder) {
+    DexItemFactory factory = appView.dexItemFactory();
+    DexProto newProto = isStatic ? method.getProto() : factory.prependHolderToProto(method);
+    return factory.createMethod(newHolder, newProto, method.getName());
+  }
+
   private class RetargetingSetup {
 
     private void setUpRetargeting() {
@@ -375,6 +423,7 @@
             DexType newHolder = retargetCoreLibMember.get(methodName).get(inType);
             List<DexClassAndMethod> found = findMethodsWithName(methodName, typeClass);
             for (DexClassAndMethod method : found) {
+              boolean emulatedDispatch = false;
               DexMethod methodReference = method.getReference();
               if (!typeClass.isFinal()) {
                 nonFinalHolderRewrites.putIfAbsent(method.getName(), new ArrayList<>());
@@ -386,12 +435,17 @@
                   } else if (!method.getAccessFlags().isFinal()) {
                     // Virtual rewrites require emulated dispatch for inheritance.
                     // The call is rewritten to the dispatch holder class instead.
-                    handleEmulateDispatch(appView, method);
-                    newHolder = dispatchHolderTypeFor(method);
+                    emulatedDispatchMethods.add(method);
+                    emulatedDispatch = true;
                   }
                 }
               }
-              retargetLibraryMember.put(methodReference, computeRetargetMethod(method, newHolder));
+              if (!emulatedDispatch) {
+                retargetLibraryMember.put(
+                    methodReference,
+                    computeRetargetMethod(
+                        methodReference, method.getAccessFlags().isStatic(), newHolder));
+              }
             }
           }
         }
@@ -408,13 +462,12 @@
             itemFactory.createMethod(
                 itemFactory.createType(itemFactory.arraysDescriptor), proto, name);
         DexMethod target =
-            itemFactory.createMethod(
-                itemFactory.createType("Ljava/util/DesugarArrays;"), proto, name);
+            computeRetargetMethod(
+                source, true, itemFactory.createType("Ljava/util/DesugarArrays;"));
         retargetLibraryMember.put(source, target);
 
         // TODO(b/181629049): This is only a workaround rewriting invokes of
         //  j.u.TimeZone.getTimeZone taking a java.time.ZoneId.
-        // to j.u.DesugarArrays.deepEquals0.
         name = itemFactory.createString("getTimeZone");
         proto =
             itemFactory.createProto(
@@ -423,8 +476,8 @@
         source =
             itemFactory.createMethod(itemFactory.createType("Ljava/util/TimeZone;"), proto, name);
         target =
-            itemFactory.createMethod(
-                itemFactory.createType("Ljava/util/DesugarTimeZone;"), proto, name);
+            computeRetargetMethod(
+                source, true, itemFactory.createType("Ljava/util/DesugarTimeZone;"));
         retargetLibraryMember.put(source, target);
       }
     }
@@ -460,15 +513,6 @@
       return false;
     }
 
-    private DexMethod computeRetargetMethod(DexClassAndMethod method, DexType newHolder) {
-      DexItemFactory factory = appView.dexItemFactory();
-      DexProto newProto =
-          method.getAccessFlags().isStatic()
-              ? method.getProto()
-              : factory.prependHolderToProto(method.getReference());
-      return factory.createMethod(newHolder, newProto, method.getName());
-    }
-
     private List<DexClassAndMethod> findMethodsWithName(DexString methodName, DexClass clazz) {
       List<DexClassAndMethod> found = new ArrayList<>();
       clazz.forEachClassMethodMatching(
@@ -476,23 +520,160 @@
       assert !found.isEmpty() : "Should have found a method (library specifications).";
       return found;
     }
-
-    private void handleEmulateDispatch(AppView<?> appView, DexClassAndMethod method) {
-      emulatedDispatchMethods.add(method);
-      if (!appView.options().isDesugaredLibraryCompilation()) {
-        // Add rewrite rules so keeps rules are correctly generated in the program.
-        DexType dispatchInterfaceType = dispatchInterfaceTypeFor(method);
-        appView.rewritePrefix.rewriteType(dispatchInterfaceType, dispatchInterfaceType);
-        DexType dispatchHolderType = dispatchHolderTypeFor(method);
-        appView.rewritePrefix.rewriteType(dispatchHolderType, dispatchHolderType);
-      }
-    }
   }
 
-  public void synthesizeRetargetClasses(
-      DexApplication.Builder<?> builder, ExecutorService executorService, IRConverter converter)
+  public void finalizeDesugaring(DesugaredLibraryRetargeterEventConsumer eventConsumer) {
+    new EmulatedDispatchTreeFixer().fixApp(eventConsumer);
+  }
+
+  private void rewriteType(DexType type) {
+    String newName =
+        appView.options().desugaredLibraryConfiguration.convertJavaNameToDesugaredLibrary(type);
+    DexType newType =
+        appView.dexItemFactory().createType(DescriptorUtils.javaTypeToDescriptor(newName));
+    appView.rewritePrefix.rewriteType(type, newType);
+  }
+
+  public DexClass ensureEmulatedHolderDispatchMethod(
+      DexClassAndMethod emulatedDispatchMethod,
+      DesugaredLibraryRetargeterEventConsumer eventConsumer) {
+    assert eventConsumer != null || appView.enableWholeProgramOptimizations();
+    DexClass interfaceClass =
+        ensureEmulatedInterfaceDispatchMethod(emulatedDispatchMethod, eventConsumer);
+    DexMethod itfMethod =
+        interfaceClass.lookupMethod(emulatedDispatchMethod.getReference()).getReference();
+    DexClass holderDispatch;
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      holderDispatch =
+          appView
+              .getSyntheticItems()
+              .ensureFixedClass(
+                  SyntheticKind.RETARGET_CLASS,
+                  emulatedDispatchMethod.getHolder(),
+                  appView,
+                  classBuilder ->
+                      buildHolderDispatchMethod(classBuilder, emulatedDispatchMethod, itfMethod),
+                  clazz -> {
+                    if (eventConsumer != null) {
+                      eventConsumer.acceptDesugaredLibraryRetargeterDispatchProgramClass(clazz);
+                    }
+                  });
+    } else {
+      ClasspathOrLibraryClass context =
+          emulatedDispatchMethod.getHolder().asClasspathOrLibraryClass();
+      assert context != null;
+      holderDispatch =
+          appView
+              .getSyntheticItems()
+              .ensureFixedClasspathClass(
+                  SyntheticKind.RETARGET_CLASS,
+                  context,
+                  appView,
+                  classBuilder ->
+                      buildHolderDispatchMethod(classBuilder, emulatedDispatchMethod, itfMethod),
+                  clazz -> {
+                    if (eventConsumer != null) {
+                      eventConsumer.acceptDesugaredLibraryRetargeterDispatchClasspathClass(clazz);
+                    }
+                  });
+    }
+    rewriteType(holderDispatch.type);
+    return holderDispatch;
+  }
+
+  public DexClass ensureEmulatedInterfaceDispatchMethod(
+      DexClassAndMethod emulatedDispatchMethod,
+      DesugaredLibraryRetargeterEventConsumer eventConsumer) {
+    assert eventConsumer != null || appView.enableWholeProgramOptimizations();
+    DexClass interfaceDispatch;
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      interfaceDispatch =
+          appView
+              .getSyntheticItems()
+              .ensureFixedClass(
+                  SyntheticKind.RETARGET_INTERFACE,
+                  emulatedDispatchMethod.getHolder(),
+                  appView,
+                  classBuilder ->
+                      buildInterfaceDispatchMethod(classBuilder, emulatedDispatchMethod),
+                  clazz -> {
+                    if (eventConsumer != null) {
+                      eventConsumer.acceptDesugaredLibraryRetargeterDispatchProgramClass(clazz);
+                    }
+                  });
+    } else {
+      ClasspathOrLibraryClass context =
+          emulatedDispatchMethod.getHolder().asClasspathOrLibraryClass();
+      assert context != null;
+      interfaceDispatch =
+          appView
+              .getSyntheticItems()
+              .ensureFixedClasspathClass(
+                  SyntheticKind.RETARGET_INTERFACE,
+                  context,
+                  appView,
+                  classBuilder ->
+                      buildInterfaceDispatchMethod(classBuilder, emulatedDispatchMethod),
+                  clazz -> {
+                    if (eventConsumer != null) {
+                      eventConsumer.acceptDesugaredLibraryRetargeterDispatchClasspathClass(clazz);
+                    }
+                  });
+    }
+    rewriteType(interfaceDispatch.type);
+    return interfaceDispatch;
+  }
+
+  private void buildInterfaceDispatchMethod(
+      SyntheticClassBuilder<?, ?> classBuilder, DexClassAndMethod emulatedDispatchMethod) {
+    classBuilder
+        .setInterface()
+        .addMethod(
+            methodBuilder -> {
+              MethodAccessFlags flags =
+                  MethodAccessFlags.fromSharedAccessFlags(
+                      Constants.ACC_PUBLIC | Constants.ACC_ABSTRACT | Constants.ACC_SYNTHETIC,
+                      false);
+              methodBuilder
+                  .setName(emulatedDispatchMethod.getName())
+                  .setProto(emulatedDispatchMethod.getProto())
+                  .setAccessFlags(flags);
+            });
+  }
+
+  private <SCB extends SyntheticClassBuilder<?, ?>> void buildHolderDispatchMethod(
+      SCB classBuilder, DexClassAndMethod emulatedDispatchMethod, DexMethod itfMethod) {
+    classBuilder.addMethod(
+        methodBuilder -> {
+          DexMethod desugarMethod =
+              appView
+                  .options()
+                  .desugaredLibraryConfiguration
+                  .retargetMethod(emulatedDispatchMethod, appView);
+          assert desugarMethod
+              != null; // This method is reached only for retarget core lib members.
+          methodBuilder
+              .setName(emulatedDispatchMethod.getName())
+              .setProto(desugarMethod.proto)
+              .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+              .setCode(
+                  methodSig ->
+                      new EmulateInterfaceSyntheticCfCodeProvider(
+                              emulatedDispatchMethod.getHolderType(),
+                              desugarMethod,
+                              itfMethod,
+                              Collections.emptyList(),
+                              appView)
+                          .generateCfCode());
+        });
+  }
+
+  @Deprecated // Use Cf to Cf desugaring.
+  public void synthesizeRetargetClasses(IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
-    new EmulatedDispatchTreeFixer().fixApp(builder, executorService, converter);
+    assert appView.enableWholeProgramOptimizations();
+    new EmulatedDispatchTreeFixer().fixApp(null);
+    converter.processMethodsConcurrently(forwardingMethods, executorService);
   }
 
   // The rewrite of virtual calls requires to go through emulate dispatch. This class is responsible
@@ -500,29 +681,23 @@
   // synthesize the interfaces and emulated dispatch classes in the desugared library.
   class EmulatedDispatchTreeFixer {
 
-    void fixApp(
-        DexApplication.Builder<?> builder, ExecutorService executorService, IRConverter converter)
-        throws ExecutionException {
+    void fixApp(DesugaredLibraryRetargeterEventConsumer eventConsumer) {
       if (appView.options().isDesugaredLibraryCompilation()) {
-        synthesizeEmulatedDispatchMethods(builder);
+        synthesizeEmulatedDispatchMethods(eventConsumer);
       } else {
-        addInterfacesAndForwardingMethods(executorService, converter);
+        addInterfacesAndForwardingMethods(eventConsumer);
       }
     }
 
     private void addInterfacesAndForwardingMethods(
-        ExecutorService executorService, IRConverter converter) throws ExecutionException {
+        DesugaredLibraryRetargeterEventConsumer eventConsumer) {
       assert !appView.options().isDesugaredLibraryCompilation();
       Map<DexType, List<DexClassAndMethod>> map = Maps.newIdentityHashMap();
       for (DexClassAndMethod emulatedDispatchMethod : emulatedDispatchMethods) {
         map.putIfAbsent(emulatedDispatchMethod.getHolderType(), new ArrayList<>(1));
         map.get(emulatedDispatchMethod.getHolderType()).add(emulatedDispatchMethod);
       }
-      SortedProgramMethodSet addedMethods = SortedProgramMethodSet.create();
       for (DexProgramClass clazz : appView.appInfo().classes()) {
-        if (appView.isAlreadyLibraryDesugared(clazz)) {
-          continue;
-        }
         if (clazz.superType == null) {
           assert clazz.type == appView.dexItemFactory().objectType : clazz.type.toSourceString();
           continue;
@@ -536,13 +711,11 @@
           map.forEach(
               (type, methods) -> {
                 if (inherit(superclass.asLibraryClass(), type, emulatedDispatchMethods)) {
-                  addInterfacesAndForwardingMethods(
-                      clazz, methods, method -> addedMethods.createAndAdd(clazz, method));
+                  addInterfacesAndForwardingMethods(eventConsumer, clazz, methods);
                 }
               });
         }
       }
-      converter.processMethodsConcurrently(addedMethods, executorService);
     }
 
     private boolean inherit(
@@ -566,20 +739,29 @@
     }
 
     private void addInterfacesAndForwardingMethods(
+        DesugaredLibraryRetargeterEventConsumer eventConsumer,
         DexProgramClass clazz,
-        List<DexClassAndMethod> methods,
-        Consumer<DexEncodedMethod> newForwardingMethodsConsumer) {
+        List<DexClassAndMethod> methods) {
       // DesugaredLibraryRetargeter emulate dispatch: insertion of a marker interface & forwarding
       // methods.
       // We cannot use the ClassProcessor since this applies up to 26, while the ClassProcessor
       // applies up to 24.
       for (DexClassAndMethod method : methods) {
-        clazz.addExtraInterfaces(
-            Collections.singletonList(new ClassTypeSignature(dispatchInterfaceTypeFor(method))));
+        DexClass dexClass = ensureEmulatedInterfaceDispatchMethod(method, eventConsumer);
+        if (clazz.interfaces.contains(dexClass.type)) {
+          // The class has already been desugared.
+          continue;
+        }
+        clazz.addExtraInterfaces(Collections.singletonList(new ClassTypeSignature(dexClass.type)));
         if (clazz.lookupVirtualMethod(method.getReference()) == null) {
           DexEncodedMethod newMethod = createForwardingMethod(method, clazz);
           clazz.addVirtualMethod(newMethod);
-          newForwardingMethodsConsumer.accept(newMethod);
+          if (eventConsumer != null) {
+            eventConsumer.acceptForwardingMethod(new ProgramMethod(clazz, newMethod));
+          } else {
+            assert appView.enableWholeProgramOptimizations();
+            forwardingMethods.add(new ProgramMethod(clazz, newMethod));
+          }
         }
       }
     }
@@ -596,156 +778,38 @@
           target, clazz, forwardMethod, appView.dexItemFactory());
     }
 
-    private void synthesizeEmulatedDispatchMethods(DexApplication.Builder<?> builder) {
+    private void synthesizeEmulatedDispatchMethods(
+        DesugaredLibraryRetargeterEventConsumer eventConsumer) {
       assert appView.options().isDesugaredLibraryCompilation();
       if (emulatedDispatchMethods.isEmpty()) {
         return;
       }
-      ClassAccessFlags itfAccessFlags =
-          ClassAccessFlags.fromSharedAccessFlags(
-              Constants.ACC_PUBLIC
-                  | Constants.ACC_SYNTHETIC
-                  | Constants.ACC_ABSTRACT
-                  | Constants.ACC_INTERFACE);
-      ClassAccessFlags holderAccessFlags =
-          ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
       for (DexClassAndMethod emulatedDispatchMethod : emulatedDispatchMethods) {
-        // Dispatch interface.
-        DexType interfaceType = dispatchInterfaceTypeFor(emulatedDispatchMethod);
-        DexEncodedMethod itfMethod =
-            generateInterfaceDispatchMethod(emulatedDispatchMethod, interfaceType);
-        synthesizeClassWithUniqueMethod(
-            builder,
-            itfAccessFlags,
-            interfaceType,
-            itfMethod,
-            "desugared library dispatch interface",
-            appView);
-        // Dispatch holder.
-        DexType holderType = dispatchHolderTypeFor(emulatedDispatchMethod);
-        DexEncodedMethod dispatchMethod =
-            generateHolderDispatchMethod(
-                emulatedDispatchMethod, holderType, itfMethod.getReference());
-        synthesizeClassWithUniqueMethod(
-            builder,
-            holderAccessFlags,
-            holderType,
-            dispatchMethod,
-            "desugared library dispatch class",
-            appView);
+        ensureEmulatedHolderDispatchMethod(emulatedDispatchMethod, eventConsumer);
       }
     }
 
-    private DexEncodedMethod generateInterfaceDispatchMethod(
-        DexClassAndMethod emulatedDispatchMethod, DexType interfaceType) {
-      MethodAccessFlags flags =
-          MethodAccessFlags.fromSharedAccessFlags(
-              Constants.ACC_PUBLIC | Constants.ACC_ABSTRACT | Constants.ACC_SYNTHETIC, false);
-      DexMethod newMethod =
-          appView
-              .dexItemFactory()
-              .createMethod(
-                  interfaceType,
-                  emulatedDispatchMethod.getProto(),
-                  emulatedDispatchMethod.getName());
-      return new DexEncodedMethod(
-          newMethod,
-          flags,
-          MethodTypeSignature.noSignature(),
-          DexAnnotationSet.empty(),
-          ParameterAnnotationsList.empty(),
-          null,
-          true);
+    private void reportInvalidLibrarySupertype(
+        DexLibraryClass libraryClass, DexClassAndMethodSet retarget) {
+      DexClass dexClass = appView.definitionFor(libraryClass.superType);
+      String message;
+      if (dexClass == null) {
+        message = "missing";
+      } else if (dexClass.isClasspathClass()) {
+        message = "a classpath class";
+      } else {
+        message = "INVALID";
+        assert false;
+      }
+      appView
+          .options()
+          .warningInvalidLibrarySuperclassForDesugar(
+              dexClass == null ? libraryClass.getOrigin() : dexClass.getOrigin(),
+              libraryClass.type,
+              libraryClass.superType,
+              message,
+              retarget);
     }
 
-    private DexEncodedMethod generateHolderDispatchMethod(
-        DexClassAndMethod emulatedDispatchMethod, DexType dispatchHolder, DexMethod itfMethod) {
-      // The method should look like:
-      // static foo(rcvr, arg0, arg1) {
-      //    if (rcvr instanceof interfaceType) {
-      //      return invoke-interface receiver.foo(arg0, arg1);
-      //    } else {
-      //      return DesugarX.foo(rcvr, arg0, arg1)
-      //    }
-      // We do not deal with complex cases (multiple retargeting of the same signature in the
-      // same inheritance tree, etc., since they do not happen in the most common desugared library.
-      DexMethod desugarMethod =
-          appView
-              .options()
-              .desugaredLibraryConfiguration
-              .retargetMethod(emulatedDispatchMethod, appView);
-      assert desugarMethod != null; // This method is reached only for retarget core lib members.
-      DexMethod newMethod =
-          appView
-              .dexItemFactory()
-              .createMethod(dispatchHolder, desugarMethod.proto, emulatedDispatchMethod.getName());
-      CfCode code =
-          new EmulateInterfaceSyntheticCfCodeProvider(
-                  emulatedDispatchMethod.getHolderType(),
-                  desugarMethod,
-                  itfMethod,
-                  Collections.emptyList(),
-                  appView)
-              .generateCfCode();
-      return new DexEncodedMethod(
-          newMethod,
-          MethodAccessFlags.createPublicStaticSynthetic(),
-          MethodTypeSignature.noSignature(),
-          DexAnnotationSet.empty(),
-          ParameterAnnotationsList.empty(),
-          code,
-          true);
-    }
-  }
-
-  private void reportInvalidLibrarySupertype(
-      DexLibraryClass libraryClass, DexClassAndMethodSet retarget) {
-    DexClass dexClass = appView.definitionFor(libraryClass.superType);
-    String message;
-    if (dexClass == null) {
-      message = "missing";
-    } else if (dexClass.isClasspathClass()) {
-      message = "a classpath class";
-    } else {
-      message = "INVALID";
-      assert false;
-    }
-    appView
-        .options()
-        .warningInvalidLibrarySuperclassForDesugar(
-            dexClass == null ? libraryClass.getOrigin() : dexClass.getOrigin(),
-            libraryClass.type,
-            libraryClass.superType,
-            message,
-            retarget);
-  }
-
-  private DexType dispatchInterfaceTypeFor(DexClassAndMethod method) {
-    return dispatchTypeFor(method, "dispatchInterface");
-  }
-
-  private DexType dispatchHolderTypeFor(DexClassAndMethod method) {
-    return dispatchTypeFor(method, "dispatchHolder");
-  }
-
-  public static String getRetargetPackageAndClassPrefixDescriptor(
-      DesugaredLibraryConfiguration config) {
-    return "L"
-        + config.getSynthesizedLibraryClassesPackagePrefix()
-        + RETARGET_PACKAGE
-        + DESUGAR_LIB_RETARGET_CLASS_NAME_PREFIX;
-  }
-
-  private DexType dispatchTypeFor(DexClassAndMethod method, String suffix) {
-    String descriptor =
-        packageAndClassDescriptorPrefix
-            + '$'
-            + method.getHolderType().getName()
-            + '$'
-            + method.getName()
-            + '$'
-            + suffix
-            + ';';
-    return appView.dexItemFactory().createSynthesizedType(descriptor);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeterEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeterEventConsumer.java
new file mode 100644
index 0000000..3806aa6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeterEventConsumer.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar;
+
+import com.android.tools.r8.graph.DexClasspathClass;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface DesugaredLibraryRetargeterEventConsumer {
+
+  void acceptDesugaredLibraryRetargeterDispatchProgramClass(DexProgramClass clazz);
+
+  void acceptDesugaredLibraryRetargeterDispatchClasspathClass(DexClasspathClass clazz);
+
+  void acceptForwardingMethod(ProgramMethod method);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
index c8b6147..5b25c84 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/EmptyCfInstructionDesugaringCollection.java
@@ -58,6 +58,11 @@
   }
 
   @Override
+  public void withDesugaredLibraryRetargeter(Consumer<DesugaredLibraryRetargeter> consumer) {
+    // Intentionally empty.
+  }
+
+  @Override
   public void withRecordRewriter(Consumer<RecordRewriter> consumer) {
     // Intentionally empty.
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index 0d4a470..bb345a4 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -39,11 +39,20 @@
 
   private final NestBasedAccessDesugaring nestBasedAccessDesugaring;
   private final RecordRewriter recordRewriter;
+  private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
 
   NonEmptyCfInstructionDesugaringCollection(AppView<?> appView) {
     this.appView = appView;
     this.nestBasedAccessDesugaring = NestBasedAccessDesugaring.create(appView);
     BackportedMethodRewriter backportedMethodRewriter = null;
+    desugaredLibraryRetargeter =
+        appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember().isEmpty()
+                || appView.enableWholeProgramOptimizations()
+            ? null
+            : new DesugaredLibraryRetargeter(appView);
+    if (desugaredLibraryRetargeter != null) {
+      desugarings.add(desugaredLibraryRetargeter);
+    }
     if (appView.options().enableBackportedMethodRewriting()) {
       backportedMethodRewriter = new BackportedMethodRewriter(appView);
     }
@@ -54,7 +63,9 @@
     // TODO(b/183998768): Enable interface method rewriter cf to cf also in R8.
     if (appView.options().isInterfaceMethodDesugaringEnabled()
         && !appView.enableWholeProgramOptimizations()) {
-      desugarings.add(new InterfaceMethodRewriter(appView, backportedMethodRewriter));
+      desugarings.add(
+          new InterfaceMethodRewriter(
+              appView, backportedMethodRewriter, desugaredLibraryRetargeter));
     }
     desugarings.add(new LambdaInstructionDesugaring(appView));
     desugarings.add(new InvokeSpecialToSelfDesugaring(appView));
@@ -80,6 +91,7 @@
       AppView<?> appView, InvokeSpecialToSelfDesugaring invokeSpecialToSelfDesugaring) {
     this.appView = appView;
     this.nestBasedAccessDesugaring = null;
+    this.desugaredLibraryRetargeter = null;
     this.recordRewriter = null;
     desugarings.add(invokeSpecialToSelfDesugaring);
   }
@@ -282,6 +294,13 @@
   }
 
   @Override
+  public void withDesugaredLibraryRetargeter(Consumer<DesugaredLibraryRetargeter> consumer) {
+    if (desugaredLibraryRetargeter != null) {
+      consumer.accept(desugaredLibraryRetargeter);
+    }
+  }
+
+  @Override
   public void withRecordRewriter(Consumer<RecordRewriter> consumer) {
     if (recordRewriter != null) {
       consumer.accept(recordRewriter);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceProcessor.java
index 5929db5..cf86667 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceProcessor.java
@@ -21,7 +21,6 @@
 import com.android.tools.r8.ir.synthetic.EmulateInterfaceSyntheticCfCodeProvider;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
 import com.android.tools.r8.synthesis.SyntheticNaming;
-import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -153,7 +152,8 @@
                             builder.addMethod(
                                 methodBuilder ->
                                     synthesizeEmulatedInterfaceMethod(
-                                        method, emulatedInterface, methodBuilder))));
+                                        method, emulatedInterface, methodBuilder))),
+                ignored -> {});
     emulateInterfaceClass.forEachProgramMethod(synthesizedMethods::add);
     assert emulateInterfaceClass.getType()
         == InterfaceMethodRewriter.getEmulateLibraryInterfaceClassType(
@@ -308,19 +308,11 @@
           && !rewriter.isEmulatedInterface(clazz.type)
           && !appView.rewritePrefix.hasRewrittenType(clazz.type, appView)
           && isEmulatedInterfaceSubInterface(clazz)) {
-        String prefix =
-            DescriptorUtils.getJavaTypeFromBinaryName(
-                appView
-                    .options()
-                    .desugaredLibraryConfiguration
-                    .getSynthesizedLibraryClassesPackagePrefix());
-        String interfaceType = clazz.type.toString();
-        // TODO(b/183918843): We are currently computing a new name for the companion class
-        // by replacing the initial package prefix by the synthesized library class package
-        // prefix, it would be better to make the rewriting explicit in the desugared library
-        // json file.
-        int firstPackage = interfaceType.indexOf('.');
-        String newName = prefix + interfaceType.substring(firstPackage + 1);
+        String newName =
+            appView
+                .options()
+                .desugaredLibraryConfiguration
+                .convertJavaNameToDesugaredLibrary(clazz.type);
         rewriter.addCompanionClassRewriteRule(clazz.type, newName);
       } else {
         filteredProgramClasses.add(clazz);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index bdd6064..79afbf8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -9,7 +9,6 @@
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
 import static com.android.tools.r8.ir.code.Invoke.Type.SUPER;
 import static com.android.tools.r8.ir.code.Invoke.Type.VIRTUAL;
-import static com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter.getRetargetPackageAndClassPrefixDescriptor;
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer.TYPE_WRAPPER_SUFFIX;
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer.VIVIFIED_TYPE_WRAPPER_SUFFIX;
 
@@ -60,6 +59,7 @@
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
@@ -150,6 +150,7 @@
 
   // This is used to filter out double desugaring on backported methods.
   private final BackportedMethodRewriter backportedMethodRewriter;
+  private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
 
   /** Defines a minor variation in desugaring. */
   public enum Flavor {
@@ -160,10 +161,14 @@
   }
 
   // Constructor for cf to cf desugaring.
-  public InterfaceMethodRewriter(AppView<?> appView, BackportedMethodRewriter rewriter) {
+  public InterfaceMethodRewriter(
+      AppView<?> appView,
+      BackportedMethodRewriter rewriter,
+      DesugaredLibraryRetargeter desugaredLibraryRetargeter) {
     this.appView = appView;
     this.converter = null;
     this.backportedMethodRewriter = rewriter;
+    this.desugaredLibraryRetargeter = desugaredLibraryRetargeter;
     this.options = appView.options();
     this.factory = appView.dexItemFactory();
     this.emulatedInterfaces = options.desugaredLibraryConfiguration.getEmulateLibraryInterface();
@@ -177,6 +182,7 @@
     this.appView = appView;
     this.converter = converter;
     this.backportedMethodRewriter = null;
+    this.desugaredLibraryRetargeter = null;
     this.options = appView.options();
     this.factory = appView.dexItemFactory();
     this.emulatedInterfaces = options.desugaredLibraryConfiguration.getEmulateLibraryInterface();
@@ -282,6 +288,17 @@
     return true;
   }
 
+  private boolean isAlreadyRewritten(
+      DexMethod method, boolean itfBit, boolean isSuper, ProgramMethod context) {
+
+    // In Cf to Cf it is forbidden to desugar twice the same instruction, if the backported
+    // method rewriter or the desugared library retargeter already desugar the instruction, they
+    // take precedence and nothing has to be done here.
+    return (backportedMethodRewriter != null && backportedMethodRewriter.methodIsBackport(method))
+        || (desugaredLibraryRetargeter != null
+            && desugaredLibraryRetargeter.hasNewInvokeTarget(method, itfBit, isSuper, context));
+  }
+
   @Override
   public boolean hasPreciseNeedsDesugaring() {
     return false;
@@ -308,7 +325,11 @@
       }
       if (instruction.isInvoke()) {
         CfInvoke cfInvoke = instruction.asInvoke();
-        if (backportedMethodRewriter.methodIsBackport(cfInvoke.getMethod())) {
+        if (isAlreadyRewritten(
+            cfInvoke.getMethod(),
+            cfInvoke.isInterface(),
+            cfInvoke.isInvokeSuper(context.getHolderType()),
+            context)) {
           continue;
         }
         if (cfInvoke.isInvokeStatic()) {
@@ -379,6 +400,13 @@
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
+      if (isAlreadyRewritten(
+          cfInvoke.getMethod(),
+          cfInvoke.isInterface(),
+          cfInvoke.isInvokeSuper(context.getHolderType()),
+          context)) {
+        return false;
+      }
       return needsRewriting(cfInvoke.getMethod(), cfInvoke.getInvokeType(context), context);
     }
     return false;
@@ -397,7 +425,11 @@
       return null;
     }
     CfInvoke invoke = instruction.asInvoke();
-    if (backportedMethodRewriter.methodIsBackport(invoke.getMethod())) {
+    if (isAlreadyRewritten(
+        invoke.getMethod(),
+        invoke.isInterface(),
+        invoke.isInvokeSuper(context.getHolderType()),
+        context)) {
       return null;
     }
 
@@ -409,7 +441,12 @@
     Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow =
         (resolutionResult) ->
             rewriteInvokeToThrowCf(
-                invoke, resolutionResult, eventConsumer, context, methodProcessingContext);
+                invoke,
+                resolutionResult,
+                localStackAllocator,
+                eventConsumer,
+                context,
+                methodProcessingContext);
 
     if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
       AppInfoWithClassHierarchy appInfoForDesugaring = appView.appInfoForDesugaring();
@@ -451,14 +488,15 @@
   private Collection<CfInstruction> rewriteInvokeToThrowCf(
       CfInvoke invoke,
       SingleResolutionResult resolutionResult,
+      LocalStackAllocator localStackAllocator,
       CfInstructionDesugaringEventConsumer eventConsumer,
       ProgramMethod context,
       MethodProcessingContext methodProcessingContext) {
-    if (backportedMethodRewriter != null
-        && backportedMethodRewriter.methodIsBackport(invoke.getMethod())) {
-      // In Cf to Cf it is not allowed to desugar twice the same instruction, if the backported
-      // method rewriter already desugars the instruction, it takes precedence and nothing has
-      // to be done here.
+    if (isAlreadyRewritten(
+        invoke.getMethod(),
+        invoke.isInterface(),
+        invoke.isInvokeSuper(context.getHolderType()),
+        context)) {
       return null;
     }
 
@@ -517,6 +555,10 @@
           returnType.isPrimitiveType()
               ? new CfConstNumber(0, ValueType.fromDexType(returnType))
               : new CfConstNull());
+    } else {
+      // If the return type is void, the stack may need an extra slot to fit the return type of
+      // the call to the throwing method.
+      localStackAllocator.allocateLocalStack(1);
     }
     return replacement;
   }
@@ -770,11 +812,7 @@
           // to outline again the invoke-static. Just do nothing instead.
           return null;
         }
-        if (backportedMethodRewriter != null
-            && backportedMethodRewriter.methodIsBackport(invokedMethod)) {
-          // In Cf to Cf it is not allowed to desugar twice the same instruction, if the backported
-          // method rewriter already desugars the instruction, it takes precedence and nothing has
-          // to be done here.
+        if (isAlreadyRewritten(invokedMethod, interfaceBit, false, context)) {
           return null;
         }
         ProgramMethod newProgramMethod =
@@ -1216,16 +1254,15 @@
     }
     appView
         .getSyntheticItems()
-        .ensureDirectMethodOnSyntheticClasspathClassWhileMigrating(
+        .ensureFixedClasspathClassMethod(
+            rewritten.getName(),
+            rewritten.getProto(),
             SyntheticKind.COMPANION_CLASS,
-            rewritten.getHolderType(),
             context,
             appView,
-            rewritten,
-            builder ->
-                builder
-                    .setName(rewritten.name)
-                    .setProto(rewritten.proto)
+            classBuilder -> {},
+            methodBuilder ->
+                methodBuilder
                     .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
                     .setCode(DexEncodedMethod::buildEmptyThrowingCfCode));
   }
@@ -1323,9 +1360,6 @@
   private Predicate<DexType> getShouldIgnoreFromReportsPredicate(AppView<?> appView) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     InternalOptions options = appView.options();
-    DexString retargetPackageAndClassPrefixDescriptor =
-        dexItemFactory.createString(
-            getRetargetPackageAndClassPrefixDescriptor(options.desugaredLibraryConfiguration));
     DexString typeWrapperClassNameDescriptorSuffix =
         dexItemFactory.createString(TYPE_WRAPPER_SUFFIX + ';');
     DexString vivifiedTypeWrapperClassNameDescriptorSuffix =
@@ -1341,8 +1375,7 @@
           || descriptor.endsWith(companionClassNameDescriptorSuffix)
           || emulatedInterfaces.containsValue(type)
           || options.desugaredLibraryConfiguration.getCustomConversions().containsValue(type)
-          || appView.getDontWarnConfiguration().matches(type)
-          || descriptor.startsWith(retargetPackageAndClassPrefixDescriptor);
+          || appView.getDontWarnConfiguration().matches(type);
     };
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
index 6ba6863..2174002 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
@@ -27,6 +27,8 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.FieldAccessFlags;
@@ -39,8 +41,7 @@
 import com.android.tools.r8.graph.NestedGraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
-import com.android.tools.r8.synthesis.SyntheticNaming;
-import com.android.tools.r8.synthesis.SyntheticProgramClassBuilder;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
@@ -58,6 +59,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
 import org.objectweb.asm.Opcodes;
 
 // Default and static method interface desugaring processor for interfaces.
@@ -86,9 +88,7 @@
       return;
     }
     analyzeBridges(iface);
-    if (needsCompanionClass(iface)) {
-      ensureCompanionClass(iface, synthesizedMethods);
-    }
+    ensureCompanionClassMethods(iface, synthesizedMethods);
   }
 
   private void analyzeBridges(DexProgramClass iface) {
@@ -101,66 +101,58 @@
     }
   }
 
-  private boolean needsCompanionClass(DexProgramClass iface) {
-    if (hasStaticMethodThatTriggersNonTrivialClassInitializer(iface)) {
-      return true;
-    }
-    for (ProgramMethod method : iface.virtualProgramMethods()) {
-      DexEncodedMethod virtual = method.getDefinition();
-      if (rewriter.isDefaultMethod(virtual)) {
-        return true;
-      }
-    }
-    for (ProgramMethod method : iface.directProgramMethods()) {
-      DexEncodedMethod definition = method.getDefinition();
-      if (!definition.isInitializer()) {
-        return true;
-      }
-    }
-    return false;
+  private void ensureCompanionClassMethods(
+      DexProgramClass iface, ProgramMethodSet synthesizedMethods) {
+    ensureCompanionClassInitializesInterface(iface, synthesizedMethods);
+    // TODO(b/183998768): Once fixed, the methods should be added for processing.
+    // D8 and R8 don't need to optimize the methods since they are just moved from interfaces and
+    // don't need to be re-processed.
+    processVirtualInterfaceMethods(iface);
+    processDirectInterfaceMethods(iface);
   }
 
-  private DexProgramClass ensureCompanionClass(
-      DexProgramClass iface, ProgramMethodSet synthesizedMethods) {
-
-    DexProgramClass companionClass =
+  private ProgramMethod ensureCompanionMethod(
+      DexProgramClass iface,
+      DexString methodName,
+      DexProto methodProto,
+      Consumer<SyntheticMethodBuilder> fn) {
+    ProgramMethod method =
         appView
             .getSyntheticItems()
-            .ensureFixedClass(
-                SyntheticNaming.SyntheticKind.COMPANION_CLASS,
+            .ensureFixedClassMethod(
+                methodName,
+                methodProto,
+                SyntheticKind.COMPANION_CLASS,
                 iface,
                 appView,
-                builder -> {
-                  builder.setSourceFile(iface.sourceFile);
-                  builder.setGenericSignature(
-                      iface.getClassSignature().toObjectBoundWithSameFormals(objectTypeSignature));
-                  ensureCompanionClassInitializesInterface(iface, builder);
-                  processVirtualInterfaceMethods(iface, builder);
-                  processDirectInterfaceMethods(iface, builder);
-                });
-    assert companionClass.getType() == rewriter.getCompanionClassType(iface.type);
-    assert companionClass.hasMethods();
-
-    // D8 and R8 don't need to optimize the methods since they are just moved from interfaces and
-    // don't need to be re-processed, besides the clinit, which has just been inserted.
-    if (companionClass.hasClassInitializer()) {
-      synthesizedMethods.add(companionClass.getProgramClassInitializer());
-    }
-
-    return companionClass;
+                builder ->
+                    builder
+                        .setSourceFile(iface.sourceFile)
+                        .setGenericSignature(
+                            iface
+                                .getClassSignature()
+                                .toObjectBoundWithSameFormals(objectTypeSignature)),
+                fn);
+    assert method.getHolderType() == rewriter.getCompanionClassType(iface.type);
+    return method;
   }
 
   private void ensureCompanionClassInitializesInterface(
-      DexProgramClass iface, SyntheticProgramClassBuilder builder) {
+      DexProgramClass iface, ProgramMethodSet synthesizedMethods) {
     if (!hasStaticMethodThatTriggersNonTrivialClassInitializer(iface)) {
       return;
     }
-    DexEncodedField clinitField = ensureStaticClinitFieldToTriggerinterfaceInitialization(iface);
-    builder.addMethod(
-        methodBuilder -> createCompanionClassInitializer(iface, clinitField, methodBuilder));
+    DexEncodedField clinitField = ensureStaticClinitFieldToTriggerInterfaceInitialization(iface);
+    ProgramMethod clinit =
+        ensureCompanionMethod(
+            iface,
+            appView.dexItemFactory().classConstructorMethodName,
+            appView.dexItemFactory().createProto(appView.dexItemFactory().voidType),
+            methodBuilder -> createCompanionClassInitializer(iface, clinitField, methodBuilder));
+    synthesizedMethods.add(clinit);
   }
 
-  private DexEncodedField ensureStaticClinitFieldToTriggerinterfaceInitialization(
+  private DexEncodedField ensureStaticClinitFieldToTriggerInterfaceInitialization(
       DexProgramClass iface) {
     DexEncodedField clinitField =
         findExistingStaticClinitFieldToTriggerInterfaceInitialization(iface);
@@ -223,16 +215,13 @@
                 ImmutableList.of(),
                 ImmutableList.of());
     methodBuilder
-        .setName(appView.dexItemFactory().classConstructorMethodName)
-        .setProto(appView.dexItemFactory().createProto(appView.dexItemFactory().voidType))
         .setAccessFlags(
             MethodAccessFlags.builder().setConstructor().setPackagePrivate().setStatic().build())
         .setCode(codeGenerator)
         .setClassFileVersion(iface.getInitialClassFileVersion());
   }
 
-  private void processVirtualInterfaceMethods(
-      DexProgramClass iface, SyntheticProgramClassBuilder builder) {
+  private void processVirtualInterfaceMethods(DexProgramClass iface) {
     for (ProgramMethod method : iface.virtualProgramMethods()) {
       DexEncodedMethod virtual = method.getDefinition();
       if (rewriter.isDefaultMethod(virtual)) {
@@ -259,11 +248,12 @@
         DexEncodedMethod.setDebugInfoWithFakeThisParameter(
             code, companionMethod.getArity(), appView);
 
-        builder.addMethod(
+        ensureCompanionMethod(
+            iface,
+            companionMethod.getName(),
+            companionMethod.getProto(),
             methodBuilder ->
                 methodBuilder
-                    .setName(companionMethod.getName())
-                    .setProto(companionMethod.getProto())
                     .setAccessFlags(newFlags)
                     .setGenericSignature(MethodTypeSignature.noSignature())
                     .setAnnotations(virtual.annotations())
@@ -279,8 +269,7 @@
     }
   }
 
-  private void processDirectInterfaceMethods(
-      DexProgramClass iface, SyntheticProgramClassBuilder builder) {
+  private void processDirectInterfaceMethods(DexProgramClass iface) {
     for (ProgramMethod method : iface.directProgramMethods()) {
       DexEncodedMethod definition = method.getDefinition();
       if (definition.isClassInitializer()) {
@@ -311,11 +300,12 @@
                 + iface.origin;
         DexMethod companionMethod = rewriter.staticAsMethodOfCompanionClass(method);
 
-        builder.addMethod(
+        ensureCompanionMethod(
+            iface,
+            companionMethod.getName(),
+            companionMethod.getProto(),
             methodBuilder ->
                 methodBuilder
-                    .setName(companionMethod.getName())
-                    .setProto(companionMethod.getProto())
                     .setAccessFlags(newFlags)
                     .setGenericSignature(definition.getGenericSignature())
                     .setAnnotations(definition.annotations())
@@ -349,11 +339,12 @@
 
       DexEncodedMethod.setDebugInfoWithFakeThisParameter(code, companionMethod.getArity(), appView);
 
-      builder.addMethod(
+      ensureCompanionMethod(
+          iface,
+          companionMethod.getName(),
+          companionMethod.getProto(),
           methodBuilder ->
               methodBuilder
-                  .setName(companionMethod.getName())
-                  .setProto(companionMethod.getProto())
                   .setAccessFlags(newFlags)
                   .setGenericSignature(definition.getGenericSignature())
                   .setAnnotations(definition.annotations())
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 bba0af1..0b47a53 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
@@ -85,9 +85,9 @@
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedInstanceFieldValueForEnumInstanceReason;
 import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedLibraryInvokeReason;
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
-import com.android.tools.r8.ir.optimize.info.UpdatableMethodOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.shaking.KeepInfoCollection;
@@ -507,7 +507,7 @@
 
           @Override
           public void fixup(
-              DexEncodedMethod method, UpdatableMethodOptimizationInfo optimizationInfo) {
+              DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) {
             optimizationInfo
                 .fixupClassTypeReferences(appView, appView.graphLens())
                 .fixupAbstractReturnValue(appView, appView.graphLens())
@@ -937,7 +937,8 @@
       // We do not unbox enums with invoke custom since it's not clear the accessibility
       // constraints would be correct if the method holding the invoke custom is moved to
       // another class.
-      assert !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod());
+      assert appView.options().isGeneratingClassFiles()
+          || !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod());
       constraint = Constraint.NEVER;
     }
 
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 4fdd8f6..7ddfbaa 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
@@ -383,13 +383,16 @@
 
   private void replaceEnumInvoke(
       InstructionListIterator iterator,
-      InvokeMethod invokeMethod,
+      InvokeMethod invoke,
       DexMethod method,
       Function<DexMethod, DexEncodedMethod> synthesizor) {
     utilityMethods.computeIfAbsent(method, synthesizor);
-    Instruction instruction =
-        new InvokeStatic(method, invokeMethod.outValue(), invokeMethod.arguments());
-    iterator.replaceCurrentInstruction(instruction);
+    InvokeStatic replacement =
+        new InvokeStatic(
+            method, invoke.hasUnusedOutValue() ? null : invoke.outValue(), invoke.arguments());
+    assert !replacement.hasOutValue()
+        || !replacement.getInvokedMethod().getReturnType().isVoidType();
+    iterator.replaceCurrentInstruction(replacement);
   }
 
   private boolean validateArrayAccess(ArrayAccess arrayAccess) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 8a43d7c..a29e290 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -24,6 +25,7 @@
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -87,6 +89,7 @@
     unboxedEnumsMethods.forEach(
         (newHolderType, movedMethods) -> {
           DexProgramClass newHolderClass = appView.definitionFor(newHolderType).asProgramClass();
+          movedMethods.sort(Comparator.comparing(DexEncodedMember::getReference));
           newHolderClass.addDirectMethods(movedMethods);
         });
     return lensBuilder.build(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
index 80fbd54..0408418 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
@@ -20,11 +20,6 @@
   }
 
   @Override
-  public MutableFieldOptimizationInfo mutableCopy() {
-    return new MutableFieldOptimizationInfo();
-  }
-
-  @Override
   public boolean cannotBeKept() {
     return false;
   }
@@ -60,12 +55,7 @@
   }
 
   @Override
-  public boolean isDefaultFieldOptimizationInfo() {
-    return true;
-  }
-
-  @Override
-  public DefaultFieldOptimizationInfo asDefaultFieldOptimizationInfo() {
-    return this;
+  public MutableFieldOptimizationInfo toMutableOptimizationInfo() {
+    return new MutableFieldOptimizationInfo();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index 2b7ef1a..1df189a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -48,21 +48,6 @@
   }
 
   @Override
-  public boolean isDefaultMethodOptimizationInfo() {
-    return true;
-  }
-
-  @Override
-  public boolean isUpdatableMethodOptimizationInfo() {
-    return false;
-  }
-
-  @Override
-  public UpdatableMethodOptimizationInfo asUpdatableMethodOptimizationInfo() {
-    return mutableCopy();
-  }
-
-  @Override
   public boolean cannotBeKept() {
     return false;
   }
@@ -204,7 +189,7 @@
   }
 
   @Override
-  public UpdatableMethodOptimizationInfo mutableCopy() {
-    return new UpdatableMethodOptimizationInfo();
+  public MutableMethodOptimizationInfo toMutableOptimizationInfo() {
+    return new MutableMethodOptimizationInfo();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java
index fc61ba6..a5e21d9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java
@@ -26,8 +26,9 @@
   }
 
   @Override
-  public UpdatableMethodOptimizationInfo mutableCopy() {
-    UpdatableMethodOptimizationInfo updatableMethodOptimizationInfo = super.mutableCopy();
+  public MutableMethodOptimizationInfo toMutableOptimizationInfo() {
+    MutableMethodOptimizationInfo updatableMethodOptimizationInfo =
+        super.toMutableOptimizationInfo();
     // Use null to specify that the min api is set to minApi.
     updatableMethodOptimizationInfo.setApiReferenceLevel(null);
     return updatableMethodOptimizationInfo;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
index e70a91f..8a098ac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
@@ -12,9 +12,8 @@
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
-public abstract class FieldOptimizationInfo {
-
-  public abstract MutableFieldOptimizationInfo mutableCopy();
+public abstract class FieldOptimizationInfo
+    implements MemberOptimizationInfo<MutableFieldOptimizationInfo> {
 
   public abstract boolean cannotBeKept();
 
@@ -56,20 +55,4 @@
   public abstract boolean isDead();
 
   public abstract boolean valueHasBeenPropagated();
-
-  public boolean isDefaultFieldOptimizationInfo() {
-    return false;
-  }
-
-  public DefaultFieldOptimizationInfo asDefaultFieldOptimizationInfo() {
-    return null;
-  }
-
-  public boolean isMutableFieldOptimizationInfo() {
-    return false;
-  }
-
-  public MutableFieldOptimizationInfo asMutableFieldOptimizationInfo() {
-    return null;
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MemberOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MemberOptimizationInfo.java
new file mode 100644
index 0000000..e9ce890
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MemberOptimizationInfo.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info;
+
+public interface MemberOptimizationInfo<
+    T extends MemberOptimizationInfo<T> & MutableOptimizationInfo> {
+
+  default boolean isMutableOptimizationInfo() {
+    return false;
+  }
+
+  default MutableMethodOptimizationInfo asMutableMethodOptimizationInfo() {
+    return null;
+  }
+
+  default MutableFieldOptimizationInfo asMutableFieldOptimizationInfo() {
+    return null;
+  }
+
+  T toMutableOptimizationInfo();
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index 0190cd0..41c145f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -21,7 +21,8 @@
 import java.util.BitSet;
 import java.util.Set;
 
-public abstract class MethodOptimizationInfo {
+public abstract class MethodOptimizationInfo
+    implements MemberOptimizationInfo<MutableMethodOptimizationInfo> {
 
   enum InlinePreference {
     NeverInline,
@@ -29,12 +30,6 @@
     Default
   }
 
-  public abstract boolean isDefaultMethodOptimizationInfo();
-
-  public abstract boolean isUpdatableMethodOptimizationInfo();
-
-  public abstract UpdatableMethodOptimizationInfo asUpdatableMethodOptimizationInfo();
-
   public abstract boolean cannotBeKept();
 
   public abstract boolean classInitializerMayBePostponed();
@@ -104,8 +99,6 @@
 
   public abstract boolean hasApiReferenceLevel();
 
-  public abstract UpdatableMethodOptimizationInfo mutableCopy();
-
   public static OptionalBool isApiSafeForInlining(
       MethodOptimizationInfo caller, MethodOptimizationInfo inlinee, InternalOptions options) {
     if (!options.apiModelingOptions().enableApiCallerIdentification) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
index b4141c6..9150ea0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -24,7 +24,8 @@
  * updated directly, meaning that updates may become visible to concurrently processed methods in
  * the {@link com.android.tools.r8.ir.conversion.IRConverter}.
  */
-public class MutableFieldOptimizationInfo extends FieldOptimizationInfo {
+public class MutableFieldOptimizationInfo extends FieldOptimizationInfo
+    implements MutableOptimizationInfo {
 
   private static final int FLAGS_CANNOT_BE_KEPT = 1 << 0;
   private static final int FLAGS_IS_DEAD = 1 << 1;
@@ -62,7 +63,6 @@
     return this;
   }
 
-  @Override
   public MutableFieldOptimizationInfo mutableCopy() {
     MutableFieldOptimizationInfo copy = new MutableFieldOptimizationInfo();
     copy.flags = flags;
@@ -137,11 +137,16 @@
   }
 
   @Override
-  public boolean isMutableFieldOptimizationInfo() {
+  public boolean isMutableOptimizationInfo() {
     return true;
   }
 
   @Override
+  public MutableFieldOptimizationInfo toMutableOptimizationInfo() {
+    return this;
+  }
+
+  @Override
   public MutableFieldOptimizationInfo asMutableFieldOptimizationInfo() {
     return this;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
similarity index 95%
rename from src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
rename to src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
index 85e2576..95d817f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableMethodOptimizationInfo.java
@@ -28,7 +28,8 @@
 import java.util.Optional;
 import java.util.Set;
 
-public class UpdatableMethodOptimizationInfo extends MethodOptimizationInfo {
+public class MutableMethodOptimizationInfo extends MethodOptimizationInfo
+    implements MutableOptimizationInfo {
 
   private Set<DexType> initializedClassesOnNormalExit =
       DefaultMethodOptimizationInfo.UNKNOWN_INITIALIZED_CLASSES_ON_NORMAL_EXIT;
@@ -130,13 +131,13 @@
 
   private int flags = DEFAULT_FLAGS;
 
-  UpdatableMethodOptimizationInfo() {
+  MutableMethodOptimizationInfo() {
     // Intentionally left empty, just use the default values.
   }
 
   // Copy constructor used to create a mutable copy. Do not forget to copy from template when a new
   // field is added.
-  private UpdatableMethodOptimizationInfo(UpdatableMethodOptimizationInfo template) {
+  private MutableMethodOptimizationInfo(MutableMethodOptimizationInfo template) {
     flags = template.flags;
     initializedClassesOnNormalExit = template.initializedClassesOnNormalExit;
     returnedArgument = template.returnedArgument;
@@ -153,12 +154,12 @@
     apiReferenceLevel = template.apiReferenceLevel;
   }
 
-  public UpdatableMethodOptimizationInfo fixupClassTypeReferences(
+  public MutableMethodOptimizationInfo fixupClassTypeReferences(
       AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
     return fixupClassTypeReferences(appView, lens, emptySet());
   }
 
-  public UpdatableMethodOptimizationInfo fixupClassTypeReferences(
+  public MutableMethodOptimizationInfo fixupClassTypeReferences(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       GraphLens lens,
       Set<DexType> prunedTypes) {
@@ -180,13 +181,13 @@
     return this;
   }
 
-  public UpdatableMethodOptimizationInfo fixupAbstractReturnValue(
+  public MutableMethodOptimizationInfo fixupAbstractReturnValue(
       AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     abstractReturnValue = abstractReturnValue.rewrittenWithLens(appView, lens);
     return this;
   }
 
-  public UpdatableMethodOptimizationInfo fixupInstanceInitializerInfo(
+  public MutableMethodOptimizationInfo fixupInstanceInitializerInfo(
       AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     instanceInitializerInfoCollection =
         instanceInitializerInfoCollection.rewrittenWithLens(appView, lens);
@@ -214,21 +215,6 @@
   }
 
   @Override
-  public boolean isDefaultMethodOptimizationInfo() {
-    return false;
-  }
-
-  @Override
-  public boolean isUpdatableMethodOptimizationInfo() {
-    return true;
-  }
-
-  @Override
-  public UpdatableMethodOptimizationInfo asUpdatableMethodOptimizationInfo() {
-    return this;
-  }
-
-  @Override
   public boolean cannotBeKept() {
     return isFlagSet(CANNOT_BE_KEPT_FLAG);
   }
@@ -511,14 +497,28 @@
     return apiReferenceLevel != null;
   }
 
-  public UpdatableMethodOptimizationInfo setApiReferenceLevel(AndroidApiLevel apiReferenceLevel) {
-    this.apiReferenceLevel = Optional.ofNullable(apiReferenceLevel);
+  @Override
+  public boolean isMutableOptimizationInfo() {
+    return true;
+  }
+
+  @Override
+  public MutableMethodOptimizationInfo toMutableOptimizationInfo() {
     return this;
   }
 
   @Override
-  public UpdatableMethodOptimizationInfo mutableCopy() {
-    return new UpdatableMethodOptimizationInfo(this);
+  public MutableMethodOptimizationInfo asMutableMethodOptimizationInfo() {
+    return this;
+  }
+
+  public MutableMethodOptimizationInfo setApiReferenceLevel(AndroidApiLevel apiReferenceLevel) {
+    this.apiReferenceLevel = Optional.ofNullable(apiReferenceLevel);
+    return this;
+  }
+
+  public MutableMethodOptimizationInfo mutableCopy() {
+    return new MutableMethodOptimizationInfo(this);
   }
 
   public void adjustOptimizationInfoAfterRemovingThisParameter() {
@@ -536,7 +536,8 @@
         || returnedArgument > 0;
     returnedArgument =
         returnedArgument == DefaultMethodOptimizationInfo.UNKNOWN_RETURNED_ARGUMENT
-            ? DefaultMethodOptimizationInfo.UNKNOWN_RETURNED_ARGUMENT : returnedArgument - 1;
+            ? DefaultMethodOptimizationInfo.UNKNOWN_RETURNED_ARGUMENT
+            : returnedArgument - 1;
     // mayHaveSideEffects: `this` Argument didn't have side effects, so removing it doesn't affect
     //   whether or not the method may have side effects.
     // returnValueOnlyDependsOnArguments:
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableOptimizationInfo.java
new file mode 100644
index 0000000..bf34966
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableOptimizationInfo.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info;
+
+public interface MutableOptimizationInfo {}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedback.java
index b384693..26516df 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedback.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.conversion.FieldOptimizationFeedback;
@@ -23,7 +24,18 @@
 
     void fixup(DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo);
 
-    void fixup(DexEncodedMethod method, UpdatableMethodOptimizationInfo optimizationInfo);
+    void fixup(DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo);
+
+    default void fixup(DexEncodedMember<?, ?> member) {
+      MemberOptimizationInfo<?> optimizationInfo = member.getOptimizationInfo();
+      if (optimizationInfo.isMutableOptimizationInfo()) {
+        member.apply(
+            field -> {
+              fixup(field, optimizationInfo.asMutableFieldOptimizationInfo());
+            },
+            method -> fixup(method, optimizationInfo.asMutableMethodOptimizationInfo()));
+      }
+    }
   }
 
   public void fixupOptimizationInfos(
@@ -38,26 +50,7 @@
       OptimizationInfoFixer fixer)
       throws ExecutionException {
     ThreadUtils.processItems(
-        classes,
-        clazz -> {
-          for (DexEncodedField field : clazz.fields()) {
-            FieldOptimizationInfo optimizationInfo = field.getOptimizationInfo();
-            if (optimizationInfo.isMutableFieldOptimizationInfo()) {
-              fixer.fixup(field, optimizationInfo.asMutableFieldOptimizationInfo());
-            } else {
-              assert optimizationInfo.isDefaultFieldOptimizationInfo();
-            }
-          }
-          for (DexEncodedMethod method : clazz.methods()) {
-            MethodOptimizationInfo optimizationInfo = method.getOptimizationInfo();
-            if (optimizationInfo.isUpdatableMethodOptimizationInfo()) {
-              fixer.fixup(method, optimizationInfo.asUpdatableMethodOptimizationInfo());
-            } else {
-              assert optimizationInfo.isDefaultMethodOptimizationInfo();
-            }
-          }
-        },
-        executorService);
+        classes, clazz -> clazz.members().forEach(fixer::fixup), executorService);
   }
 
   public void modifyAppInfoWithLiveness(Consumer<AppInfoWithLivenessModifier> consumer) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index 5a12983..dd781e8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -36,7 +36,7 @@
       AppInfoWithLiveness.modifier();
   private final Map<DexEncodedField, MutableFieldOptimizationInfo> fieldOptimizationInfos =
       new IdentityHashMap<>();
-  private final Map<DexEncodedMethod, UpdatableMethodOptimizationInfo> methodOptimizationInfos =
+  private final Map<DexEncodedMethod, MutableMethodOptimizationInfo> methodOptimizationInfos =
       new IdentityHashMap<>();
   private final Map<DexEncodedMethod, ConstraintWithTarget> processed = new IdentityHashMap<>();
 
@@ -46,24 +46,23 @@
     if (info != null) {
       return info;
     }
-    info = field.getOptimizationInfo().mutableCopy();
+    info = field.getOptimizationInfo().toMutableOptimizationInfo().mutableCopy();
     fieldOptimizationInfos.put(field, info);
     return info;
   }
 
-  private synchronized UpdatableMethodOptimizationInfo getMethodOptimizationInfoForUpdating(
+  private synchronized MutableMethodOptimizationInfo getMethodOptimizationInfoForUpdating(
       DexEncodedMethod method) {
-    UpdatableMethodOptimizationInfo info = methodOptimizationInfos.get(method);
+    MutableMethodOptimizationInfo info = methodOptimizationInfos.get(method);
     if (info != null) {
       return info;
     }
-    info = method.getOptimizationInfo().mutableCopy();
+    info = method.getOptimizationInfo().toMutableOptimizationInfo().mutableCopy();
     methodOptimizationInfos.put(method, info);
     return info;
   }
 
-  private UpdatableMethodOptimizationInfo getMethodOptimizationInfoForUpdating(
-      ProgramMethod method) {
+  private MutableMethodOptimizationInfo getMethodOptimizationInfoForUpdating(ProgramMethod method) {
     return getMethodOptimizationInfoForUpdating(method.getDefinition());
   }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
index e440049..08236e2 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassInfo.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.Reporter;
@@ -37,6 +38,7 @@
 
   private final int flags;
   private final String name;
+  private final boolean nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin;
   private final String moduleName;
   private final List<KotlinConstructorInfo> constructorsWithNoBacking;
   private final KotlinDeclarationContainerInfo declarationContainerInfo;
@@ -54,6 +56,7 @@
   private KotlinClassInfo(
       int flags,
       String name,
+      boolean nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin,
       String moduleName,
       KotlinDeclarationContainerInfo declarationContainerInfo,
       List<KotlinTypeParameterInfo> typeParameters,
@@ -69,6 +72,8 @@
       int[] metadataVersion) {
     this.flags = flags;
     this.name = name;
+    this.nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin =
+        nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin;
     this.moduleName = moduleName;
     this.declarationContainerInfo = declarationContainerInfo;
     this.typeParameters = typeParameters;
@@ -127,9 +132,17 @@
         KotlinDeclarationContainerInfo.create(
             kmClass, methodMap, fieldMap, factory, reporter, keepByteCode, extensionInformation);
     setCompanionObject(kmClass, hostClass, reporter);
+    KotlinTypeReference anonymousObjectOrigin = getAnonymousObjectOrigin(kmClass, factory);
+    boolean nameCanBeDeducedFromClassOrOrigin =
+        kmClass.name.equals(
+                KotlinMetadataUtils.getKotlinClassName(
+                    hostClass, hostClass.getType().toDescriptorString()))
+            || (anonymousObjectOrigin != null
+                && kmClass.name.equals(anonymousObjectOrigin.toKotlinClassifier(true)));
     return new KotlinClassInfo(
         kmClass.getFlags(),
         kmClass.name,
+        nameCanBeDeducedFromClassOrOrigin,
         JvmExtensionsKt.getModuleName(kmClass),
         container,
         KotlinTypeParameterInfo.create(kmClass.getTypeParameters(), factory, reporter),
@@ -139,7 +152,7 @@
         getNestedClasses(hostClass, kmClass.getNestedClasses(), factory),
         kmClass.getEnumEntries(),
         KotlinVersionRequirementInfo.create(kmClass.getVersionRequirements()),
-        getAnonymousObjectOrigin(kmClass, factory),
+        anonymousObjectOrigin,
         packageName,
         KotlinLocalDelegatedPropertyInfo.create(
             JvmExtensionsKt.getLocalDelegatedProperties(kmClass), factory, reporter),
@@ -221,14 +234,29 @@
     // Set potentially renamed class name.
     DexString originalDescriptor = clazz.type.descriptor;
     DexString rewrittenDescriptor = namingLens.lookupDescriptor(clazz.type);
-    // If the original descriptor equals the rewritten descriptor, we pick the original name
-    // to preserve potential errors in the original name. As an example, the kotlin stdlib has
-    // name: .kotlin/collections/CollectionsKt___CollectionsKt$groupingBy$1, which seems incorrect.
     boolean rewritten = !originalDescriptor.equals(rewrittenDescriptor);
-    kmClass.setName(
-        !rewritten
-            ? this.name
-            : DescriptorUtils.getBinaryNameFromDescriptor(rewrittenDescriptor.toString()));
+    if (!nameCanBeSynthesizedFromClassOrAnonymousObjectOrigin) {
+      kmClass.setName(this.name);
+    } else {
+      String rewrittenName = null;
+      // When the class has an anonymousObjectOrigin and the name equals the identifier there, we
+      // keep the name tied to the anonymousObjectOrigin.
+      if (anonymousObjectOrigin != null
+          && name.equals(anonymousObjectOrigin.toKotlinClassifier(true))) {
+        Box<String> rewrittenOrigin = new Box<>();
+        anonymousObjectOrigin.toRenamedBinaryNameOrDefault(
+            rewrittenOrigin::set, appView, namingLens, null);
+        if (rewrittenOrigin.isSet()) {
+          rewrittenName = "." + rewrittenOrigin.get();
+        }
+      }
+      if (rewrittenName == null) {
+        rewrittenName =
+            KotlinMetadataUtils.getKotlinClassName(clazz, rewrittenDescriptor.toString());
+      }
+      kmClass.setName(rewrittenName);
+      rewritten |= !name.equals(rewrittenName);
+    }
     // Find a companion object.
     for (DexEncodedField field : clazz.fields()) {
       if (field.getKotlinInfo().isCompanion()) {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
index 5f5b696..9b2939e 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassifierInfo.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getKotlinLocalOrAnonymousNameFromDescriptor;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -68,13 +70,8 @@
     boolean rewrite(KmTypeVisitor visitor, AppView<?> appView, NamingLens namingLens) {
       return type.toRenamedDescriptorOrDefault(
           descriptor -> {
-            // For local or anonymous classes, the classifier is prefixed with '.' and inner classes
-            // are separated with '$'.
-            if (isLocalOrAnonymous) {
-              visitor.visitClass("." + DescriptorUtils.getBinaryNameFromDescriptor(descriptor));
-            } else {
-              visitor.visitClass(DescriptorUtils.descriptorToKotlinClassifier(descriptor));
-            }
+            visitor.visitClass(
+                getKotlinLocalOrAnonymousNameFromDescriptor(descriptor, isLocalOrAnonymous));
           },
           appView,
           namingLens,
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
index eb654b5..1b928d9 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
@@ -179,7 +179,11 @@
     }
     for (DexEncodedMethod method : clazz.methods()) {
       if (method.getKotlinInfo().isFunction()) {
-        method.getKotlinInfo().asFunction().rewrite(functionProvider, method, appView, namingLens);
+        rewritten |=
+            method
+                .getKotlinInfo()
+                .asFunction()
+                .rewrite(functionProvider, method, appView, namingLens);
         continue;
       }
       KotlinPropertyInfo kotlinPropertyInfo = method.getKotlinInfo().asProperty();
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
index d22a2b4..a094a88 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinClassMetadataReader.toKotlinClassMetadata;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getInvalidKotlinInfo;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getNoKotlinInfo;
+import static com.android.tools.r8.kotlin.KotlinMetadataWriter.kotlinMetadataToString;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotation;
@@ -160,6 +162,15 @@
       // TODO(b/185756596): Remove when special handling is no longer needed.
       if (!kotlinClassHeader.getSecond() && !appView.enableWholeProgramOptimizations()) {
         // No rewrite occurred and the data is the same as before.
+        assert appView.checkForTesting(
+            () ->
+                verifyRewrittenMetadataIsEquivalent(
+                    clazz.annotations().getFirstMatching(factory.kotlinMetadataType),
+                    createKotlinMetadataAnnotation(
+                        kotlinClassHeader.getFirst(),
+                        kotlinInfo.getPackageName(),
+                        getMaxVersion(METADATA_VERSION_1_4, kotlinInfo.getMetadataVersion()),
+                        writeMetadataFieldInfo)));
         return;
       }
       DexAnnotation newMeta =
@@ -177,6 +188,16 @@
     }
   }
 
+  private boolean verifyRewrittenMetadataIsEquivalent(
+      DexAnnotation original, DexAnnotation rewritten) {
+    String originalMetadata =
+        kotlinMetadataToString("", toKotlinClassMetadata(kotlin, original.annotation));
+    String rewrittenMetadata =
+        kotlinMetadataToString("", toKotlinClassMetadata(kotlin, rewritten.annotation));
+    assert originalMetadata.equals(rewrittenMetadata) : "The metadata should be equivalent";
+    return true;
+  }
+
   private boolean kotlinMetadataFieldExists(
       DexClass kotlinMetadata, AppView<?> appView, DexString fieldName) {
     if (!appView.appInfo().hasLiveness()) {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
index 7eb45f6..2d6f39a 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataUtils.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
@@ -201,4 +202,27 @@
     // Check if the type is matched
     return proguardKeepRule.getClassNames().matches(factory.kotlinMetadataType);
   }
+
+  static String getKotlinClassName(DexClass clazz, String descriptor) {
+    InnerClassAttribute innerClassAttribute = clazz.getInnerClassAttributeForThisClass();
+    if (innerClassAttribute != null && innerClassAttribute.getOuter() != null) {
+      return DescriptorUtils.descriptorToKotlinClassifier(descriptor);
+    } else if (clazz.isLocalClass() || clazz.isAnonymousClass()) {
+      return getKotlinLocalOrAnonymousNameFromDescriptor(descriptor, true);
+    } else {
+      // We no longer have an innerclass relationship to maintain and we therefore return a binary
+      // name.
+      return DescriptorUtils.getBinaryNameFromDescriptor(descriptor);
+    }
+  }
+
+  static String getKotlinLocalOrAnonymousNameFromDescriptor(
+      String descriptor, boolean isLocalOrAnonymous) {
+    // For local or anonymous classes, the classifier is prefixed with '.' and inner classes
+    // are separated with '$'.
+    if (isLocalOrAnonymous) {
+      return "." + DescriptorUtils.getBinaryNameFromDescriptor(descriptor);
+    }
+    return DescriptorUtils.descriptorToKotlinClassifier(descriptor);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeProjectionInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeProjectionInfo.java
index 3f83e53..8fe760b 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeProjectionInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeProjectionInfo.java
@@ -42,10 +42,10 @@
       NamingLens namingLens) {
     if (isStarProjection()) {
       starProjectionProvider.get();
+      return false;
     } else {
-      typeInfo.rewrite(flags -> visitorProvider.get(flags, variance), appView, namingLens);
+      return typeInfo.rewrite(flags -> visitorProvider.get(flags, variance), appView, namingLens);
     }
-    return false;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
index 4d0d990..c166103 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeReference.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.kotlin.KotlinMetadataUtils.getKotlinLocalOrAnonymousNameFromDescriptor;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -81,6 +83,14 @@
     return !known.toDescriptorString().equals(renamedString);
   }
 
+  String toKotlinClassifier(boolean isLocalOrAnonymous) {
+    if (known == null) {
+      return unknown;
+    }
+    return getKotlinLocalOrAnonymousNameFromDescriptor(
+        known.toDescriptorString(), isLocalOrAnonymous);
+  }
+
   boolean toRenamedBinaryNameOrDefault(
       Consumer<String> rewrittenConsumer,
       AppView<?> appView,
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 239c0da..a4478d6 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -47,7 +47,6 @@
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
-import com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.synthesis.CommittedItems;
@@ -522,7 +521,6 @@
             // TODO(b/150693139): Remove these exceptions once fixed.
             || InterfaceMethodRewriter.isCompanionClassType(type)
             || InterfaceMethodRewriter.isEmulatedLibraryClassType(type)
-            || DesugaredLibraryRetargeter.isRetargetType(type, options())
             // TODO(b/150736225): Not sure how to remove these.
             || DesugaredLibraryAPIConverter.isVivifiedType(type)
         : "Failed lookup of non-missing type: " + type;
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 61db1bf..743fa3c 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1813,6 +1813,8 @@
       enqueueFirstNonSerializableClassInitializer(clazz, reason);
     }
 
+    checkDefinitionForSoftPinning(clazz);
+
     processAnnotations(clazz);
 
     // If this type has deferred annotations, we have to process those now, too.
@@ -2618,7 +2620,7 @@
     // Add all dependent members to the workqueue.
     enqueueRootItems(rootSet.getDependentItems(field.getDefinition()));
 
-    checkMemberForSoftPinning(field);
+    checkDefinitionForSoftPinning(field);
 
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveField(field, context));
@@ -3868,7 +3870,7 @@
     // Add all dependent members to the workqueue.
     enqueueRootItems(rootSet.getDependentItems(definition));
 
-    checkMemberForSoftPinning(method);
+    checkDefinitionForSoftPinning(method);
 
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method, context));
@@ -3930,15 +3932,15 @@
         methodDefinition.getMutableOptimizationInfo().setApiReferenceLevel(maxApiReferenceLevel));
   }
 
-  private void checkMemberForSoftPinning(ProgramMember<?, ?> member) {
-    DexMember<?, ?> reference = member.getDefinition().getReference();
+  private void checkDefinitionForSoftPinning(ProgramDefinition definition) {
+    DexReference reference = definition.getReference();
     Set<ProguardKeepRuleBase> softPinRules = rootSet.softPinned.getRulesForReference(reference);
     if (softPinRules != null) {
       assert softPinRules.stream().noneMatch(r -> r.getModifiers().allowsOptimization);
       keepInfo.joinInfo(reference, appInfo, Joiner::pin);
     }
     // Identify dependent soft pinning.
-    MutableItemsWithRules items = rootSet.dependentSoftPinned.get(member.getHolderType());
+    MutableItemsWithRules items = rootSet.dependentSoftPinned.get(definition.getContextType());
     if (items != null && items.containsReference(reference)) {
       assert items.getRulesForReference(reference).stream()
           .noneMatch(r -> r.getModifiers().allowsOptimization);
diff --git a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
index 0ec8c27..2cc626d 100644
--- a/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
+++ b/src/main/java/com/android/tools/r8/shaking/MissingClasses.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.shaking;
 
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter.DESCRIPTOR_VIVIFIED_PREFIX;
-import static com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter.getRetargetPackageAndClassPrefixDescriptor;
 import static com.android.tools.r8.utils.collections.IdentityHashSetFromMap.newProgramDerivedContextSet;
 
 import com.android.tools.r8.diagnostic.MissingDefinitionsDiagnostic;
@@ -23,7 +22,6 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.synthesis.CommittedItems;
 import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -280,15 +278,10 @@
     private static Predicate<DexType> getIsCompilerSynthesizedAllowedMissingClassesPredicate(
         AppView<?> appView) {
       DexItemFactory dexItemFactory = appView.dexItemFactory();
-      InternalOptions options = appView.options();
-      DexString retargetPackageAndClassPrefixDescriptor =
-          dexItemFactory.createString(
-              getRetargetPackageAndClassPrefixDescriptor(options.desugaredLibraryConfiguration));
       DexString vivifiedClassNamePrefix = dexItemFactory.createString(DESCRIPTOR_VIVIFIED_PREFIX);
       return type -> {
         DexString descriptor = type.getDescriptor();
-        return descriptor.startsWith(retargetPackageAndClassPrefixDescriptor)
-            || descriptor.startsWith(vivifiedClassNamePrefix);
+        return descriptor.startsWith(vivifiedClassNamePrefix);
       };
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index 6d3a8ca..49c4a1e 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -378,15 +378,15 @@
         // For Proguard -keepattributes are only applicable when obfuscating.
         keepAttributePatterns.addAll(ProguardKeepAttributes.KEEP_ALL);
       }
-      // If either of the flags -dontshrink, -dontobfuscate or -dontoptimize is passed, or
-      // shrinking or minification is turned off through the API, then add a match all rule
-      // which will apply that.
-      if (!isShrinking() || !isObfuscating() || !isOptimizing()) {
+      // If either of the flags -dontshrink or -dontobfuscate, or shrinking or minification is
+      // turned off through the API, then add a match all rule which will apply that.
+      if (!isShrinking() || !isObfuscating()) {
         ProguardKeepRule rule =
             ProguardKeepRule.defaultKeepAllRule(
                 modifiers -> {
                   modifiers.setAllowsShrinking(isShrinking());
-                  modifiers.setAllowsOptimization(isOptimizing());
+                  // TODO(b/189807246): This should be removed.
+                  modifiers.setAllowsOptimization(true);
                   modifiers.setAllowsObfuscation(isObfuscating());
                 });
         addRule(rule);
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index ef2f149..df989fe 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -18,10 +18,10 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
-import com.android.tools.r8.ir.optimize.info.UpdatableMethodOptimizationInfo;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
@@ -402,7 +402,7 @@
 
           @Override
           public void fixup(
-              DexEncodedMethod method, UpdatableMethodOptimizationInfo optimizationInfo) {
+              DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) {
             optimizationInfo.fixupClassTypeReferences(appView, appView.graphLens(), prunedTypes);
           }
         });
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 800332f..e0837dd 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -368,6 +368,9 @@
       }
       return false;
     }
+    if (sourceClass.isAnnotation()) {
+      return false;
+    }
     if (!sourceClass.isInterface()
         && targetClass.isSerializable(appView)
         && !appInfo.isSerializable(sourceClass.type)) {
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 8e78b09..65c510c 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -22,19 +22,23 @@
 import com.android.tools.r8.graph.NestHostClassAttribute;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Consumer;
 
-abstract class SyntheticClassBuilder<B extends SyntheticClassBuilder<B, C>, C extends DexClass> {
+public abstract class SyntheticClassBuilder<
+    B extends SyntheticClassBuilder<B, C>, C extends DexClass> {
 
   private final DexItemFactory factory;
 
   private final DexType type;
+  private final SyntheticKind syntheticKind;
   private final Origin origin;
 
   private boolean isAbstract = false;
+  private boolean isInterface = false;
   private Kind originKind;
   private DexType superType;
   private DexTypeList interfaces = DexTypeList.empty();
@@ -46,9 +50,14 @@
   private List<SyntheticMethodBuilder> methods = new ArrayList<>();
   private ClassSignature signature = ClassSignature.noSignature();
 
-  SyntheticClassBuilder(DexType type, SynthesizingContext context, DexItemFactory factory) {
+  SyntheticClassBuilder(
+      DexType type,
+      SyntheticKind syntheticKind,
+      SynthesizingContext context,
+      DexItemFactory factory) {
     this.factory = factory;
     this.type = type;
+    this.syntheticKind = syntheticKind;
     this.origin = context.getInputContextOrigin();
     this.superType = factory.objectType;
   }
@@ -65,6 +74,10 @@
     return type;
   }
 
+  public SyntheticKind getSyntheticKind() {
+    return syntheticKind;
+  }
+
   public B setInterfaces(List<DexType> interfaces) {
     this.interfaces =
         interfaces.isEmpty()
@@ -78,6 +91,12 @@
     return self();
   }
 
+  public B setInterface() {
+    setAbstract();
+    isInterface = true;
+    return self();
+  }
+
   public B setOriginKind(Kind originKind) {
     this.originKind = originKind;
     return self();
@@ -126,9 +145,11 @@
 
   public C build() {
     int flag = isAbstract ? Constants.ACC_ABSTRACT : Constants.ACC_FINAL;
+    int itfFlag = isInterface ? Constants.ACC_INTERFACE : 0;
+    assert !isInterface || isAbstract;
     ClassAccessFlags accessFlags =
         ClassAccessFlags.fromSharedAccessFlags(
-            flag | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
+            flag | itfFlag | Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC);
     NestHostClassAttribute nestHost = null;
     List<NestMemberClassAttribute> nestMembers = Collections.emptyList();
     EnclosingMethodAttribute enclosingMembers = null;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClasspathClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClasspathClassBuilder.java
index 20cd075..b888913 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClasspathClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClasspathClassBuilder.java
@@ -8,13 +8,17 @@
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 
 public class SyntheticClasspathClassBuilder
     extends SyntheticClassBuilder<SyntheticClasspathClassBuilder, DexClasspathClass> {
 
   SyntheticClasspathClassBuilder(
-      DexType type, SynthesizingContext context, DexItemFactory factory) {
-    super(type, context, factory);
+      DexType type,
+      SyntheticKind syntheticKind,
+      SynthesizingContext context,
+      DexItemFactory factory) {
+    super(type, syntheticKind, context, factory);
     setOriginKind(Kind.CF);
   }
 
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 89552fb..8c37bf1 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -125,6 +125,14 @@
       methodMap.put(from, to);
     }
 
+    void setRepresentative(DexField field, DexField representative) {
+      fieldMap.setRepresentative(field, representative);
+    }
+
+    void setRepresentative(DexMethod method, DexMethod representative) {
+      methodMap.setRepresentative(method, representative);
+    }
+
     SyntheticFinalizationGraphLens build(AppView<?> appView) {
       if (typeMap.isEmpty() && fieldMap.isEmpty() && methodMap.isEmpty()) {
         return null;
@@ -477,6 +485,26 @@
                   syntheticMethodDefinition.getReference()));
         });
 
+    Iterables.<EquivalenceGroup<? extends SyntheticDefinition<?, ?, DexProgramClass>>>concat(
+            syntheticClassGroups.values(), syntheticMethodGroups.values())
+        .forEach(
+            syntheticGroup ->
+                syntheticGroup
+                    .getRepresentative()
+                    .getHolder()
+                    .forEachProgramMember(
+                        member -> {
+                          if (member.isProgramField()) {
+                            DexField field = member.asProgramField().getReference();
+                            DexField rewrittenField = treeFixer.fixupFieldReference(field);
+                            lensBuilder.setRepresentative(rewrittenField, field);
+                          } else {
+                            DexMethod method = member.asProgramMethod().getReference();
+                            DexMethod rewrittenMethod = treeFixer.fixupMethodReference(method);
+                            lensBuilder.setRepresentative(rewrittenMethod, method);
+                          }
+                        }));
+
     for (DexType key : syntheticMethodGroups.keySet()) {
       assert application.definitionFor(key) != null;
     }
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 15719ce..2d0970f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -11,13 +11,18 @@
 import com.android.tools.r8.graph.ClasspathOrLibraryClass;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
+import com.android.tools.r8.graph.MethodCollection;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
@@ -462,7 +467,7 @@
       DexType type,
       DexItemFactory factory) {
     SyntheticProgramClassBuilder classBuilder =
-        new SyntheticProgramClassBuilder(type, outerContext, factory);
+        new SyntheticProgramClassBuilder(type, kind, outerContext, factory);
     fn.accept(classBuilder);
     DexProgramClass clazz = classBuilder.build();
     addPendingDefinition(new SyntheticProgramClassDefinition(kind, outerContext, clazz));
@@ -491,7 +496,10 @@
       Consumer<SyntheticProgramClassBuilder> fn) {
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
     // This is to ensure a flat input-type -> synthetic-item mapping.
-    SynthesizingContext outerContext = getSynthesizingContext(context, appView);
+    SynthesizingContext outerContext =
+        context.isProgramClass()
+            ? getSynthesizingContext(context.asProgramClass(), appView)
+            : SynthesizingContext.fromNonSyntheticInputContext(context.asClasspathOrLibraryClass());
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
     return internalCreateClass(kind, fn, outerContext, type, appView.dexItemFactory());
   }
@@ -503,16 +511,20 @@
    */
   public DexProgramClass ensureFixedClass(
       SyntheticKind kind,
-      DexProgramClass context,
+      DexClass context,
       AppView<?> appView,
-      Consumer<SyntheticProgramClassBuilder> fn) {
+      Consumer<SyntheticProgramClassBuilder> fn,
+      Consumer<DexProgramClass> onCreationConsumer) {
     assert kind.isFixedSuffixSynthetic;
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
     // This is to ensure a flat input-type -> synthetic-item mapping.
-    SynthesizingContext outerContext = getSynthesizingContext(context, appView);
+    SynthesizingContext outerContext =
+        context.isProgramClass()
+            ? getSynthesizingContext(context.asProgramClass(), appView)
+            : SynthesizingContext.fromNonSyntheticInputContext(context.asClasspathOrLibraryClass());
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
     // Fast path is that the synthetic is already present. If so it must be a program class.
-    DexClass clazz = appView.definitionFor(type, context);
+    DexClass clazz = appView.definitionFor(type);
     if (clazz != null) {
       assert isSyntheticClass(type);
       assert clazz.isProgramClass();
@@ -521,17 +533,36 @@
     // Slow path creates the class using the context to make it thread safe.
     synchronized (context) {
       // Recheck if it is present now the lock is held.
-      clazz = appView.definitionFor(type, context);
+      clazz = appView.definitionFor(type);
       if (clazz != null) {
         assert isSyntheticClass(type);
         assert clazz.isProgramClass();
         return clazz.asProgramClass();
       }
       assert !isSyntheticClass(type);
-      return internalCreateClass(kind, fn, outerContext, type, appView.dexItemFactory());
+      DexProgramClass dexProgramClass =
+          internalCreateClass(kind, fn, outerContext, type, appView.dexItemFactory());
+      onCreationConsumer.accept(dexProgramClass);
+      return dexProgramClass;
     }
   }
 
+  public ProgramMethod ensureFixedClassMethod(
+      DexString name,
+      DexProto proto,
+      SyntheticKind kind,
+      DexProgramClass context,
+      AppView<?> appView,
+      Consumer<SyntheticProgramClassBuilder> buildClassCallback,
+      Consumer<SyntheticMethodBuilder> buildMethodCallback) {
+    DexProgramClass clazz =
+        ensureFixedClass(kind, context, appView, buildClassCallback, ignored -> {});
+    DexMethod methodReference = appView.dexItemFactory().createMethod(clazz.getType(), proto, name);
+    DexEncodedMethod methodDefinition =
+        internalEnsureMethod(methodReference, clazz, kind, appView, buildMethodCallback);
+    return new ProgramMethod(clazz, methodDefinition);
+  }
+
   public DexClasspathClass createFixedClasspathClass(
       SyntheticKind kind, DexClasspathClass context, DexItemFactory factory) {
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
@@ -539,54 +570,87 @@
     SynthesizingContext outerContext = SynthesizingContext.fromNonSyntheticInputContext(context);
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, factory);
     SyntheticClasspathClassBuilder classBuilder =
-        new SyntheticClasspathClassBuilder(type, outerContext, factory);
+        new SyntheticClasspathClassBuilder(type, kind, outerContext, factory);
     DexClasspathClass clazz = classBuilder.build();
     addPendingDefinition(new SyntheticClasspathClassDefinition(kind, outerContext, clazz));
     return clazz;
   }
 
-  // This is a temporary API for migration to the hygienic synthetic, the classes created behave
-  // like a hygienic synthetic, but use the legacyType passed as parameter instead of the
-  // hygienic type.
-  private DexClasspathClass ensureFixedClasspathClassWhileMigrating(
-      SyntheticKind kind, DexType legacyType, ClasspathOrLibraryClass context, AppView<?> appView) {
+  public DexClasspathClass ensureFixedClasspathClass(
+      SyntheticKind kind,
+      ClasspathOrLibraryClass context,
+      AppView<?> appView,
+      Consumer<SyntheticClasspathClassBuilder> classConsumer,
+      Consumer<DexClasspathClass> onCreationConsumer) {
+    SynthesizingContext outerContext = SynthesizingContext.fromNonSyntheticInputContext(context);
+    DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
+    DexClass dexClass = appView.appInfo().definitionForWithoutExistenceAssert(type);
+    if (dexClass != null) {
+      assert dexClass.isClasspathClass();
+      return dexClass.asClasspathClass();
+    }
     synchronized (context) {
-      DexClass dexClass = appView.definitionFor(legacyType);
+      dexClass = appView.appInfo().definitionForWithoutExistenceAssert(type);
       if (dexClass != null) {
         assert dexClass.isClasspathClass();
         return dexClass.asClasspathClass();
       }
       // Obtain the outer synthesizing context in the case the context itself is synthetic.
       // This is to ensure a flat input-type -> synthetic-item mapping.
-      SynthesizingContext outerContext = SynthesizingContext.fromNonSyntheticInputContext(context);
       SyntheticClasspathClassBuilder classBuilder =
-          new SyntheticClasspathClassBuilder(legacyType, outerContext, appView.dexItemFactory());
+          new SyntheticClasspathClassBuilder(type, kind, outerContext, appView.dexItemFactory());
+      classConsumer.accept(classBuilder);
       DexClasspathClass clazz = classBuilder.build();
+      onCreationConsumer.accept(clazz);
       addPendingDefinition(new SyntheticClasspathClassDefinition(kind, outerContext, clazz));
       return clazz;
     }
   }
 
-  // This is a temporary API for migration to the hygienic synthetic, the classes created behave
-  // like a hygienic synthetic, but use the legacyType passed as parameter instead of the
-  // hygienic type.
-  public void ensureDirectMethodOnSyntheticClasspathClassWhileMigrating(
+  public DexClassAndMethod ensureFixedClasspathClassMethod(
+      DexString methodName,
+      DexProto methodProto,
       SyntheticKind kind,
-      DexType legacyType,
       ClasspathOrLibraryClass context,
       AppView<?> appView,
-      DexMethod method,
-      Consumer<SyntheticMethodBuilder> builderConsumer) {
-    DexClasspathClass syntheticClass =
-        ensureFixedClasspathClassWhileMigrating(kind, legacyType, context, appView);
-    synchronized (syntheticClass) {
-      if (syntheticClass.lookupMethod(method) != null) {
-        return;
+      Consumer<SyntheticClasspathClassBuilder> buildClassCallback,
+      Consumer<SyntheticMethodBuilder> buildMethodCallback) {
+    DexClasspathClass clazz =
+        ensureFixedClasspathClass(kind, context, appView, buildClassCallback, ignored -> {});
+    DexMethod methodReference =
+        appView.dexItemFactory().createMethod(clazz.getType(), methodProto, methodName);
+    DexEncodedMethod methodDefinition =
+        internalEnsureMethod(methodReference, clazz, kind, appView, buildMethodCallback);
+    return DexClassAndMethod.create(clazz, methodDefinition);
+  }
+
+  private DexEncodedMethod internalEnsureMethod(
+      DexMethod methodReference,
+      DexClass clazz,
+      SyntheticKind kind,
+      AppView<?> appView,
+      Consumer<SyntheticMethodBuilder> buildMethodCallback) {
+    MethodCollection methodCollection = clazz.getMethodCollection();
+    DexEncodedMethod methodDefinition = methodCollection.getMethod(methodReference);
+    if (methodDefinition != null) {
+      return methodDefinition;
+    }
+    synchronized (methodCollection) {
+      methodDefinition = methodCollection.getMethod(methodReference);
+      if (methodDefinition != null) {
+        return methodDefinition;
       }
-      SyntheticMethodBuilder syntheticMethodBuilder =
-          new SyntheticMethodBuilder(appView.dexItemFactory(), syntheticClass.type);
-      builderConsumer.accept(syntheticMethodBuilder);
-      syntheticClass.addDirectMethod(syntheticMethodBuilder.build());
+      SyntheticMethodBuilder builder =
+          new SyntheticMethodBuilder(appView.dexItemFactory(), clazz.getType(), kind);
+      builder.setName(methodReference.getName());
+      builder.setProto(methodReference.getProto());
+      buildMethodCallback.accept(builder);
+      // TODO(b/183998768): Make this safe for recursive definitions.
+      //  For example, the builder should be split into the creation of the method structure
+      //  and the creation of the method code. The code can then be constructed outside the lock.
+      methodDefinition = builder.build();
+      methodCollection.addMethod(methodDefinition);
+      return methodDefinition;
     }
   }
 
@@ -625,7 +689,7 @@
         SyntheticNaming.createInternalType(
             kind, outerContext, syntheticIdSupplier.get(), appView.dexItemFactory());
     SyntheticProgramClassBuilder classBuilder =
-        new SyntheticProgramClassBuilder(type, outerContext, appView.dexItemFactory());
+        new SyntheticProgramClassBuilder(type, kind, outerContext, appView.dexItemFactory());
     DexProgramClass clazz =
         classBuilder
             .addMethod(fn.andThen(m -> m.setName(SyntheticNaming.INTERNAL_SYNTHETIC_METHOD_PREFIX)))
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index ffaad01..a06fdb7 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -128,7 +128,7 @@
         return NO_MARKER;
       }
       for (DexEncodedMethod method : clazz.methods()) {
-        if (!SyntheticMethodBuilder.isValidSyntheticMethod(method)) {
+        if (!SyntheticMethodBuilder.isValidSingleSyntheticMethod(method)) {
           return NO_MARKER;
         }
       }
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 67bd52c..e2cd6eb 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodBuilder.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import java.util.function.Consumer;
 
 public class SyntheticMethodBuilder {
@@ -25,6 +26,7 @@
 
   private final DexItemFactory factory;
   private final DexType holderType;
+  private final SyntheticKind syntheticKind;
   private DexString name = null;
   private DexProto proto = null;
   private CfVersion classFileVersion;
@@ -38,11 +40,13 @@
   SyntheticMethodBuilder(SyntheticClassBuilder<?, ?> parent) {
     this.factory = parent.getFactory();
     this.holderType = parent.getType();
+    this.syntheticKind = parent.getSyntheticKind();
   }
 
-  SyntheticMethodBuilder(DexItemFactory factory, DexType holderType) {
+  SyntheticMethodBuilder(DexItemFactory factory, DexType holderType, SyntheticKind syntheticKind) {
     this.factory = factory;
     this.holderType = holderType;
+    this.syntheticKind = syntheticKind;
   }
 
   public SyntheticMethodBuilder setName(String name) {
@@ -101,20 +105,18 @@
     assert name != null;
     boolean isCompilerSynthesized = true;
     DexMethod methodSignature = getMethodSignature();
+    MethodAccessFlags accessFlags = getAccessFlags();
     DexEncodedMethod method =
         new DexEncodedMethod(
             methodSignature,
-            getAccessFlags(),
+            accessFlags,
             genericSignature,
             annotations,
             parameterAnnotationsList,
-            getCodeObject(methodSignature),
+            accessFlags.isAbstract() ? null : getCodeObject(methodSignature),
             isCompilerSynthesized,
             classFileVersion);
-    // Companion class method may have different properties.
-    assert isValidSyntheticMethod(method)
-        || SyntheticNaming.isSynthetic(
-            holderType.asClassReference(), null, SyntheticNaming.SyntheticKind.COMPANION_CLASS);
+    assert isValidSyntheticMethod(method, syntheticKind);
     if (onBuildConsumer != null) {
       onBuildConsumer.accept(method);
     }
@@ -126,8 +128,16 @@
    *
    * <p>This method is used when identifying synthetic methods in the program input and should be as
    * narrow as possible.
+   *
+   * <p>Methods in fixed suffix synthetics are identified differently (through the class name) and
+   * can have different properties.
    */
-  public static boolean isValidSyntheticMethod(DexEncodedMethod method) {
+  public static boolean isValidSyntheticMethod(
+      DexEncodedMethod method, SyntheticKind syntheticKind) {
+    return isValidSingleSyntheticMethod(method) || syntheticKind.isFixedSuffixSynthetic;
+  }
+
+  public static boolean isValidSingleSyntheticMethod(DexEncodedMethod method) {
     return method.isStatic()
         && method.isNonAbstractNonNativeMethod()
         && method.isPublic()
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 87393f7..a9d11c7 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
@@ -70,7 +70,7 @@
 
   @Override
   public boolean isValid() {
-    return SyntheticMethodBuilder.isValidSyntheticMethod(method.getDefinition());
+    return SyntheticMethodBuilder.isValidSyntheticMethod(method.getDefinition(), getKind());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index 6ed6c0e..33530e6 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -26,6 +26,8 @@
     RECORD_TAG("", 1, false, true, true),
     COMPANION_CLASS("$-CC", 2, false, true),
     EMULATED_INTERFACE_CLASS("$-EL", 3, false, true),
+    RETARGET_CLASS("RetargetClass", 20, false, true),
+    RETARGET_INTERFACE("RetargetInterface", 21, false, true),
     LAMBDA("Lambda", 4, false),
     INIT_TYPE_ARGUMENT("-IA", 5, false, true),
     HORIZONTAL_INIT_TYPE_ARGUMENT_1(SYNTHETIC_CLASS_SEPARATOR + "IA$1", 6, false, true),
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticProgramClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticProgramClassBuilder.java
index 6408621..8c07884 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticProgramClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticProgramClassBuilder.java
@@ -7,12 +7,17 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 
 public class SyntheticProgramClassBuilder
     extends SyntheticClassBuilder<SyntheticProgramClassBuilder, DexProgramClass> {
 
-  SyntheticProgramClassBuilder(DexType type, SynthesizingContext context, DexItemFactory factory) {
-    super(type, context, factory);
+  SyntheticProgramClassBuilder(
+      DexType type,
+      SyntheticKind syntheticKind,
+      SynthesizingContext context,
+      DexItemFactory factory) {
+    super(type, syntheticKind, context, factory);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index aae7abe..4fc6914 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -87,6 +87,10 @@
     return true;
   }
 
+  public static <T> T last(T[] array) {
+    return array[array.length - 1];
+  }
+
   /**
    * Rewrites the input array based on the given function.
    *
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 e08fe12..f96e8ce 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -51,8 +51,10 @@
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.repackaging.Repackaging.DefaultRepackagingConfiguration;
 import com.android.tools.r8.repackaging.Repackaging.RepackagingConfiguration;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -315,7 +317,6 @@
   public boolean enableInitializedClassesInInstanceMethodsAnalysis = true;
   public boolean enableRedundantFieldLoadElimination = true;
   public boolean enableValuePropagation = true;
-  public boolean enableValuePropagationForInstanceFields = true;
   public boolean enableUninstantiatedTypeOptimization = true;
   // Currently disabled, see b/146957343.
   public boolean enableUninstantiatedTypeOptimizationForInterfaces = false;
@@ -1299,8 +1300,10 @@
 
   public static class ApiModelTestingOptions {
 
-    // A mapping from methods to the api-level introducing them.
+    // A mapping from references to the api-level introducing them.
     public Map<MethodReference, AndroidApiLevel> methodApiMapping = new HashMap<>();
+    public Map<FieldReference, AndroidApiLevel> fieldApiMapping = new HashMap<>();
+    public Map<TypeReference, AndroidApiLevel> typeApiMapping = new HashMap<>();
 
     public boolean enableApiCallerIdentification = false;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/PrintR8Version.java b/src/main/java/com/android/tools/r8/utils/PrintR8Version.java
new file mode 100644
index 0000000..c20d74c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/PrintR8Version.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.Version;
+
+public class PrintR8Version {
+
+  public static void main(String[] args) {
+    System.out.println(Version.getVersionString());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
index 773c066..37b9d40 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
@@ -14,6 +14,8 @@
 public interface BidirectionalManyToManyRepresentativeMap<K, V>
     extends BidirectionalManyToManyMap<K, V> {
 
+  boolean hasExplicitRepresentativeKey(V value);
+
   K getRepresentativeKey(V value);
 
   default K getRepresentativeKeyOrDefault(V value, K defaultValue) {
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java
index 1e53a14..10f520f5 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java
@@ -6,6 +6,7 @@
 
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
@@ -21,6 +22,10 @@
     return new BidirectionalManyToOneHashMap<>(new IdentityHashMap<>(), new IdentityHashMap<>());
   }
 
+  public static <K, V> BidirectionalManyToOneHashMap<K, V> newLinkedHashMap() {
+    return new BidirectionalManyToOneHashMap<>(new LinkedHashMap<>(), new LinkedHashMap<>());
+  }
+
   protected BidirectionalManyToOneHashMap(Map<K, V> backing, Map<V, Set<K>> inverse) {
     this.backing = backing;
     this.inverse = inverse;
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
index ffd653e..bdd70c8 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.collections;
 
+import com.android.tools.r8.utils.TriConsumer;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -33,6 +34,12 @@
   }
 
   @Override
+  public void forEachManyToOneMapping(TriConsumer<? super Set<K>, V, K> consumer) {
+    forEachManyToOneMapping(
+        (keys, value) -> consumer.accept(keys, value, getRepresentativeKey(value)));
+  }
+
+  @Override
   public K removeRepresentativeFor(V value) {
     return representatives.remove(value);
   }
@@ -43,10 +50,19 @@
   }
 
   @Override
+  public boolean hasExplicitRepresentativeKey(V value) {
+    return representatives.containsKey(value);
+  }
+
+  @Override
   public K getRepresentativeKey(V value) {
     Set<K> keys = getKeys(value);
     if (!keys.isEmpty()) {
-      return keys.size() == 1 ? keys.iterator().next() : representatives.get(value);
+      if (keys.size() == 1) {
+        return keys.iterator().next();
+      }
+      assert hasExplicitRepresentativeKey(value);
+      return representatives.get(value);
     }
     return null;
   }
@@ -67,8 +83,10 @@
   @Override
   public V remove(K key) {
     V value = super.remove(key);
-    if (getKeys(value).size() <= 1 || getRepresentativeKey(value) == key) {
-      removeRepresentativeFor(value);
+    if (hasExplicitRepresentativeKey(value)) {
+      if (getKeys(value).size() <= 1 || getRepresentativeKey(value) == key) {
+        removeRepresentativeFor(value);
+      }
     }
     return value;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java
index 3e7bf7b..9202cc1 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.utils.collections;
 
+import com.android.tools.r8.utils.TriConsumer;
+import java.util.Set;
+
 /**
  * Interface that accommodates many-to-one mappings.
  *
@@ -11,4 +14,7 @@
  * from {@link BidirectionalManyToManyRepresentativeMap}.
  */
 public interface BidirectionalManyToOneRepresentativeMap<K, V>
-    extends BidirectionalManyToOneMap<K, V>, BidirectionalManyToManyRepresentativeMap<K, V> {}
+    extends BidirectionalManyToOneMap<K, V>, BidirectionalManyToManyRepresentativeMap<K, V> {
+
+  void forEachManyToOneMapping(TriConsumer<? super Set<K>, V, K> consumer);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java
index 93562e7..a4974d8 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java
@@ -22,6 +22,12 @@
   }
 
   @Override
+  public boolean hasExplicitRepresentativeKey(V value) {
+    assert containsValue(value);
+    return true;
+  }
+
+  @Override
   public K getRepresentativeKey(V value) {
     return getKey(value);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
index e23a565..2961111 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.collections;
 
+import com.android.tools.r8.utils.TriConsumer;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import java.util.Collections;
@@ -61,6 +62,11 @@
   }
 
   @Override
+  public void forEachManyToOneMapping(TriConsumer<? super Set<K>, V, K> consumer) {
+    backing.forEach((key, value) -> consumer.accept(Collections.singleton(key), value, key));
+  }
+
+  @Override
   public void forEachValue(Consumer<? super V> consumer) {
     backing.values().forEach(consumer);
   }
@@ -92,6 +98,12 @@
   }
 
   @Override
+  public boolean hasExplicitRepresentativeKey(V value) {
+    assert containsValue(value);
+    return true;
+  }
+
+  @Override
   public K getRepresentativeKey(V value) {
     return getKey(value);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
index 797d8b6..4838419 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexClassAndMethodSetBase.java
@@ -40,6 +40,10 @@
     methods.forEach(this::add);
   }
 
+  public T get(DexMethod method) {
+    return backing.get(method);
+  }
+
   public boolean contains(DexEncodedMethod method) {
     return backing.containsKey(method.getReference());
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java
index 62fba9a..5f81584 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.collections;
 
+import com.android.tools.r8.utils.TriConsumer;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import java.util.Collections;
@@ -42,6 +43,11 @@
   }
 
   @Override
+  public void forEachManyToOneMapping(TriConsumer<? super Set<K>, V, K> consumer) {
+    // Intentionally empty.
+  }
+
+  @Override
   public void forEachValue(Consumer<? super V> consumer) {
     // Intentionally empty.
   }
@@ -67,6 +73,11 @@
   }
 
   @Override
+  public boolean hasExplicitRepresentativeKey(V value) {
+    return false;
+  }
+
+  @Override
   public K getRepresentativeKey(V value) {
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/KotlinTestParameters.java b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
index 8c4d024..57658e1 100644
--- a/src/test/java/com/android/tools/r8/KotlinTestParameters.java
+++ b/src/test/java/com/android/tools/r8/KotlinTestParameters.java
@@ -32,6 +32,14 @@
     return targetVersion;
   }
 
+  public boolean is(KotlinCompilerVersion compilerVersion) {
+    return kotlinc.is(compilerVersion);
+  }
+
+  public boolean is(KotlinCompilerVersion compilerVersion, KotlinTargetVersion targetVersion) {
+    return is(compilerVersion) && this.targetVersion == targetVersion;
+  }
+
   public boolean isFirst() {
     return index == 0;
   }
diff --git a/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java b/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java
index 2a0826a..1a1a532 100644
--- a/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java
+++ b/src/test/java/com/android/tools/r8/ResourceShrinkerTest.java
@@ -9,6 +9,8 @@
 import static org.junit.Assert.assertEquals;
 
 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.smali.SmaliBuilder;
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.Lists;
@@ -21,6 +23,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -34,10 +37,12 @@
   public TemporaryFolder tmp = new TemporaryFolder();
 
   private static class TrackAll implements ResourceShrinker.ReferenceChecker {
-    Set<Integer> integers = Sets.newHashSet();
-    Set<String> strings = Sets.newHashSet();
-    List<List<String>> fields = Lists.newArrayList();
-    List<List<String>> methods = Lists.newArrayList();
+    Set<Integer> refIntegers = Sets.newHashSet();
+    Set<String> refStrings = Sets.newHashSet();
+    List<List<String>> refFields = Lists.newArrayList();
+    List<List<String>> refMethods = Lists.newArrayList();
+    List<MethodReference> methodsVisited = Lists.newArrayList();
+    List<ClassReference> classesVisited = Lists.newArrayList();
 
     @Override
     public boolean shouldProcess(String internalName) {
@@ -46,17 +51,17 @@
 
     @Override
     public void referencedInt(int value) {
-      integers.add(value);
+      refIntegers.add(value);
     }
 
     @Override
     public void referencedString(String value) {
-      strings.add(value);
+      refStrings.add(value);
     }
 
     @Override
     public void referencedStaticField(String internalName, String fieldName) {
-      fields.add(Lists.newArrayList(internalName, fieldName));
+      refFields.add(Lists.newArrayList(internalName, fieldName));
     }
 
     @Override
@@ -65,7 +70,27 @@
           && Objects.equals(methodName, "<init>")) {
         return;
       }
-      methods.add(Lists.newArrayList(internalName, methodName, methodDescriptor));
+      refMethods.add(Lists.newArrayList(internalName, methodName, methodDescriptor));
+    }
+
+    @Override
+    public void startMethodVisit(MethodReference methodReference) {
+      methodsVisited.add(methodReference);
+    }
+
+    @Override
+    public void endMethodVisit(MethodReference methodReference) {
+      assertEquals(methodsVisited.get(methodsVisited.size() - 1), methodReference);
+    }
+
+    @Override
+    public void startClassVisit(ClassReference classReference) {
+      classesVisited.add(classReference);
+    }
+
+    @Override
+    public void endClassVisit(ClassReference classReference) {
+      assertEquals(classesVisited.get(classesVisited.size() - 1), classReference);
     }
   }
 
@@ -76,10 +101,10 @@
   public void testEmptyClass() throws CompilationFailedException, IOException, ExecutionException {
     TrackAll analysis = runAnalysis(EmptyClass.class);
 
-    assertThat(analysis.integers, is(Sets.newHashSet()));
-    assertThat(analysis.strings, is(Sets.newHashSet()));
-    assertThat(analysis.fields, is(Lists.newArrayList()));
-    assertThat(analysis.methods, is(Lists.newArrayList()));
+    assertThat(analysis.refIntegers, is(Sets.newHashSet()));
+    assertThat(analysis.refStrings, is(Sets.newHashSet()));
+    assertThat(analysis.refFields, is(Lists.newArrayList()));
+    assertThat(analysis.refMethods, is(Lists.newArrayList()));
   }
 
   private static class ConstInCode {
@@ -97,20 +122,21 @@
       throws CompilationFailedException, IOException, ExecutionException {
     TrackAll analysis = runAnalysis(ConstInCode.class);
 
-    assertThat(analysis.integers, is(Sets.newHashSet(10, 11)));
-    assertThat(analysis.strings, is(Sets.newHashSet("my_layout", "another_layout")));
+    assertThat(analysis.refIntegers, is(Sets.newHashSet(10, 11)));
+    assertThat(analysis.refStrings, is(Sets.newHashSet("my_layout", "another_layout")));
 
-    assertEquals(3, analysis.fields.size());
-    assertThat(analysis.fields.get(0), is(Lists.newArrayList("java/lang/System", "out")));
-    assertThat(analysis.fields.get(1), is(Lists.newArrayList("java/lang/System", "out")));
-    assertThat(analysis.fields.get(2), is(Lists.newArrayList("java/lang/System", "out")));
+    assertEquals(3, analysis.refFields.size());
+    assertThat(analysis.refFields.get(0), is(Lists.newArrayList("java/lang/System", "out")));
+    assertThat(analysis.refFields.get(1), is(Lists.newArrayList("java/lang/System", "out")));
+    assertThat(analysis.refFields.get(2), is(Lists.newArrayList("java/lang/System", "out")));
 
-    assertEquals(3, analysis.methods.size());
-    assertThat(analysis.methods.get(0),
-        is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
-    assertThat(analysis.methods.get(1),
-        is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
-    assertThat(analysis.methods.get(2),
+    assertEquals(3, analysis.refMethods.size());
+    assertThat(
+        analysis.refMethods.get(0), is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
+    assertThat(
+        analysis.refMethods.get(1), is(Lists.newArrayList("java/io/PrintStream", "print", "(I)V")));
+    assertThat(
+        analysis.refMethods.get(2),
         is(Lists.newArrayList("java/io/PrintStream", "print", "(Ljava/lang/String;)V")));
   }
 
@@ -127,10 +153,43 @@
       throws CompilationFailedException, IOException, ExecutionException {
     TrackAll analysis = runAnalysis(StaticFields.class);
 
-    assertThat(analysis.integers, hasItems(10, 11, 12, 13));
-    assertThat(analysis.strings, hasItems("staticValue", "a", "b", "c"));
-    assertThat(analysis.fields, is(Lists.newArrayList()));
-    assertThat(analysis.methods, is(Lists.newArrayList()));
+    assertThat(analysis.refIntegers, hasItems(10, 11, 12, 13));
+    assertThat(analysis.refStrings, hasItems("staticValue", "a", "b", "c"));
+    assertThat(analysis.refFields, is(Lists.newArrayList()));
+    assertThat(analysis.refMethods, is(Lists.newArrayList()));
+  }
+
+  @SuppressWarnings("unused")
+  private static class ClassesAndMethodsVisited {
+    final int value = getValue();
+
+    static final int staticValue;
+
+    static {
+      staticValue = 42;
+    }
+
+    int getValue() {
+      return true ? 0 : 1;
+    }
+  }
+
+  @Test
+  public void testNumberOfMethodsAndClassesVisited()
+      throws CompilationFailedException, IOException, ExecutionException {
+    TrackAll analysis = runAnalysis(ClassesAndMethodsVisited.class);
+
+    List<String> methodNames =
+        analysis.methodsVisited.stream()
+            .map(MethodReference::getMethodName)
+            .collect(Collectors.toList());
+    List<String> classNames =
+        analysis.classesVisited.stream()
+            .map(ClassReference::getBinaryName)
+            .collect(Collectors.toList());
+    assertThat(methodNames, hasItems("<init>", "<clinit>", "getValue"));
+    assertThat(
+        classNames, hasItems("com/android/tools/r8/ResourceShrinkerTest$ClassesAndMethodsVisited"));
   }
 
   @Retention(RetentionPolicy.RUNTIME)
@@ -165,10 +224,10 @@
   public void testAnnotations() throws CompilationFailedException, IOException, ExecutionException {
     TrackAll analysis = runAnalysis(IntAnnotation.class, OuterAnnotation.class, Annotated.class);
 
-    assertThat(analysis.integers, hasItems(10, 11, 12, 13, 14, 15, 42));
-    assertThat(analysis.strings, is(Sets.newHashSet()));
-    assertThat(analysis.fields, is(Lists.newArrayList()));
-    assertThat(analysis.methods, is(Lists.newArrayList()));
+    assertThat(analysis.refIntegers, hasItems(10, 11, 12, 13, 14, 15, 42));
+    assertThat(analysis.refStrings, is(Sets.newHashSet()));
+    assertThat(analysis.refFields, is(Lists.newArrayList()));
+    assertThat(analysis.refMethods, is(Lists.newArrayList()));
   }
 
   private static class ResourceClassToSkip {
@@ -185,10 +244,10 @@
       throws ExecutionException, CompilationFailedException, IOException {
     TrackAll analysis = runAnalysis(ResourceClassToSkip.class, ToProcess.class);
 
-    assertThat(analysis.integers, hasItems(10, 11, 12));
-    assertThat(analysis.strings, is(Sets.newHashSet("10", "11", "12")));
-    assertThat(analysis.fields, is(Lists.newArrayList()));
-    assertThat(analysis.methods, is(Lists.newArrayList()));
+    assertThat(analysis.refIntegers, hasItems(10, 11, 12));
+    assertThat(analysis.refStrings, is(Sets.newHashSet("10", "11", "12")));
+    assertThat(analysis.refFields, is(Lists.newArrayList()));
+    assertThat(analysis.refMethods, is(Lists.newArrayList()));
   }
 
   @Test
@@ -213,10 +272,10 @@
         AndroidApp.builder().addDexProgramData(builder.compile(), Origin.unknown()).build();
     TrackAll analysis = runOnApp(app);
 
-    assertThat(analysis.integers, hasItems(4, 5, 6));
-    assertThat(analysis.strings, is(Sets.newHashSet()));
-    assertThat(analysis.fields, is(Lists.newArrayList()));
-    assertThat(analysis.methods, is(Lists.newArrayList()));
+    assertThat(analysis.refIntegers, hasItems(4, 5, 6));
+    assertThat(analysis.refStrings, is(Sets.newHashSet()));
+    assertThat(analysis.refFields, is(Lists.newArrayList()));
+    assertThat(analysis.refMethods, is(Lists.newArrayList()));
   }
 
   private TrackAll runAnalysis(Class<?>... classes)
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 5aef2ad..8b99ca8 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -12,10 +12,12 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 // Base class for the runtime structure in the test parameters.
@@ -69,6 +71,19 @@
       Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "openjdk-9.0.4");
   private static final Path JDK11_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-11");
   private static final Path JDK15_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-15");
+  private static final Map<CfVm, Path> jdkPaths =
+      ImmutableMap.of(
+          CfVm.JDK8, JDK8_PATH,
+          CfVm.JDK9, JDK9_PATH,
+          CfVm.JDK11, JDK11_PATH,
+          CfVm.JDK15, JDK15_PATH);
+
+  public static CfRuntime getCheckedInJdk(CfVm vm) {
+    if (vm == CfVm.JDK8) {
+      return getCheckedInJdk8();
+    }
+    return new CfRuntime(vm, getCheckedInJdkHome(vm));
+  }
 
   public static CfRuntime getCheckedInJdk8() {
     Path home;
@@ -83,7 +98,9 @@
     return new CfRuntime(CfVm.JDK8, home);
   }
 
-  private static Path getCheckedInJdkHome(Path path, CfVm vm) {
+  private static Path getCheckedInJdkHome(CfVm vm) {
+    Path path = jdkPaths.get(vm);
+    assert path != null : "No JDK path defined for " + vm;
     if (ToolHelper.isLinux()) {
       return path.resolve("linux");
     } else if (ToolHelper.isMac()) {
@@ -97,16 +114,16 @@
   }
 
   public static CfRuntime getCheckedInJdk9() {
-    return new CfRuntime(CfVm.JDK9, getCheckedInJdkHome(JDK9_PATH, CfVm.JDK9));
+    return new CfRuntime(CfVm.JDK9, getCheckedInJdkHome(CfVm.JDK9));
   }
 
   public static CfRuntime getCheckedInJdk11() {
-    return new CfRuntime(CfVm.JDK11, getCheckedInJdkHome(JDK11_PATH, CfVm.JDK11));
+    return new CfRuntime(CfVm.JDK11, getCheckedInJdkHome(CfVm.JDK11));
   }
 
   // TODO(b/169692487): Add this to 'getCheckedInCfRuntimes' when we start having support for JDK15.
   public static CfRuntime getCheckedInJdk15() {
-    return new CfRuntime(CfVm.JDK15, getCheckedInJdkHome(JDK15_PATH, CfVm.JDK15));
+    return new CfRuntime(CfVm.JDK15, getCheckedInJdkHome(CfVm.JDK15));
   }
 
   public static List<CfRuntime> getCheckedInCfRuntimes() {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 4c765be..50f5346 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_4_20;
 import static com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion.KOTLINC_1_5_0_M2;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.JAVA_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -100,6 +101,7 @@
   public static final String THIRD_PARTY_DIR = "third_party/";
   public static final String TOOLS_DIR = "tools/";
   public static final String TESTS_DIR = "src/test/";
+  public static final String TESTS_SOURCE_DIR = "src/test/java";
   public static final String EXAMPLES_DIR = TESTS_DIR + "examples/";
   public static final String EXAMPLES_ANDROID_N_DIR = TESTS_DIR + "examplesAndroidN/";
   public static final String EXAMPLES_ANDROID_O_DIR = TESTS_DIR + "examplesAndroidO/";
@@ -1099,6 +1101,17 @@
         .collect(Collectors.toList());
   }
 
+  public static Path getSourceFileForTestClass(Class<?> clazz) {
+    List<String> parts = getNamePartsForTestClass(clazz);
+    String last = parts.get(parts.size() - 1);
+    assert last.endsWith(CLASS_EXTENSION);
+    parts.set(
+        parts.size() - 1,
+        last.substring(0, last.length() - CLASS_EXTENSION.length()) + JAVA_EXTENSION);
+    return Paths.get(TESTS_SOURCE_DIR)
+        .resolve(Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY)));
+  }
+
   public static Path getClassFileForTestClass(Class<?> clazz) {
     List<String> parts = getNamePartsForTestClass(clazz);
     return getClassPathForTests().resolve(Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY)));
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiFieldsTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiFieldsTest.java
new file mode 100644
index 0000000..8045b48
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiFieldsTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForType;
+
+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.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+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 ApiModelClassMergingWithDifferentApiFieldsTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"Api::foo", "B::bar"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelClassMergingWithDifferentApiFieldsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, ApiSetter.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        // TODO(b/138781768): Should not be merged
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertClassesMerged(A.class, B.class))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .apply(setMockApiLevelForType(Api.class, AndroidApiLevel.L_MR1))
+        .compile()
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Api {
+
+    public void foo() {
+      System.out.println("Api::foo");
+    }
+  }
+
+  static class ApiSetter {
+    static void set() {
+      A.api = new Api();
+    }
+  }
+
+  static class A {
+
+    private static Api api;
+
+    public static void callApi() throws Exception {
+      // The reflective call here is to ensure that the setting of A's api level is not based on
+      // a method reference to `Api` and only because of the type reference in the field `api`.
+      Class<?> aClass =
+          Class.forName(
+              "com.android.tools.r8.apimodeling.ApiModelClassMergingWithDifferentApiFieldsTest_Api"
+                  .replace("_", "$"));
+      Method foo = aClass.getDeclaredMethod("foo");
+      foo.invoke(api);
+    }
+  }
+
+  public static class B {
+
+    @NeverInline
+    public static void bar() {
+      System.out.println("B::bar");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      ApiSetter.set();
+      A.callApi();
+      B.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiMethodsTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiMethodsTest.java
new file mode 100644
index 0000000..b56d6ae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelClassMergingWithDifferentApiMethodsTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+
+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.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+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 ApiModelClassMergingWithDifferentApiMethodsTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"Api::apiLevel22", "B::bar"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelClassMergingWithDifferentApiMethodsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        // TODO(b/138781768): Should not be merged
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertClassesMerged(A.class, B.class))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .compile()
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Api {
+
+    public static void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  public static class A {
+
+    @NeverInline
+    public static void callFoo() {
+      Api.apiLevel22();
+    }
+  }
+
+  public static class B {
+
+    @NeverInline
+    public static void bar() {
+      System.out.println("B::bar");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.callFoo();
+      B.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelInlineMethodWithApiTypeTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelInlineMethodWithApiTypeTest.java
new file mode 100644
index 0000000..2e24790
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelInlineMethodWithApiTypeTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForType;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+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 ApiModelInlineMethodWithApiTypeTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelInlineMethodWithApiTypeTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiCallerApiLevel1 = ApiCaller.class.getDeclaredMethod("apiLevel22");
+    Method apiCallerCallerApiLevel1 = ApiCallerCaller.class.getDeclaredMethod("apiLevel22");
+    Method otherCallerApiLevel1 = OtherCaller.class.getDeclaredMethod("apiLevel1");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, OtherCaller.class, Main.class)
+        .addLibraryClasses(ApiType.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForType(ApiType.class, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .addRunClasspathClasses(ApiType.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(ApiType.class.getName())
+        .inspect(verifyThat(parameters, apiCallerApiLevel1).inlinedInto(apiCallerCallerApiLevel1))
+        // TODO(b/138781768): Should not be inlined
+        .inspect(
+            verifyThat(parameters, apiCallerCallerApiLevel1).inlinedInto(otherCallerApiLevel1));
+  }
+
+  public static class ApiType {}
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+    public static ApiType apiLevel22() throws Exception {
+      // The reflective call here is to ensure that the setting of A's api level is not based on
+      // a method reference to `Api` and only because of the type reference in the field `api`.
+      Class<?> reflectiveCall =
+          Class.forName(
+              "com.android.tools.r8.apimodeling.ApiModelInlineMethodWithApiTypeTest_ApiType"
+                  .replace("_", "$"));
+      return (ApiType) reflectiveCall.getDeclaredConstructor().newInstance();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCallerCaller {
+
+    public static void apiLevel22() throws Exception {
+      // This is referencing the proto of ApiCaller.foo and thus have a reference to ApiType. It is
+      // therefore OK to inline ApiCaller.foo() into ApiCallerCaller.bar().
+      System.out.println(ApiCaller.apiLevel22().getClass().getName());
+    }
+  }
+
+  public static class OtherCaller {
+
+    public static void apiLevel1() throws Exception {
+      // ApiCallerCaller.apiLevel22 should never be inlined here.
+      ApiCallerCaller.apiLevel22();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) throws Exception {
+      OtherCaller.apiLevel1();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningFieldTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningFieldTest.java
new file mode 100644
index 0000000..3bd5b8a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningFieldTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForField;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Field;
+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 ApiModelNoClassInliningFieldTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoClassInliningFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Field apiField = Api.class.getDeclaredField("foo");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, ApiBuilder.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForField(apiField, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            inspector -> {
+              // TODO(b/138781768): Should not be class inlined.
+              assertThat(inspector.clazz(ApiCaller.class), not(isPresent()));
+            })
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Api::apiLevel22");
+  }
+
+  public static class Api {
+
+    public String foo = "Api::apiLevel22";
+  }
+
+  public static class ApiBuilder {
+
+    public static Api build() {
+      return new Api();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    private Api api;
+
+    public ApiCaller(Api api) {
+      this.api = api;
+      System.out.println(api.foo);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCallerCaller {
+
+    @NeverInline
+    public static void callCallApi() {
+      new ApiCaller(ApiBuilder.build());
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      ApiCallerCaller.callCallApi();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningMethodTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningMethodTest.java
new file mode 100644
index 0000000..a7804e0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoClassInliningMethodTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Method;
+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 ApiModelNoClassInliningMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoClassInliningMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("callApi");
+    Method apiCallerCaller = ApiCallerCaller.class.getDeclaredMethod("callCallApi");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            verifyThat(parameters, apiCaller)
+                .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Api::apiLevel22");
+  }
+
+  public static class Api {
+
+    public static void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    public void callApi() {
+      Api.apiLevel22();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCallerCaller {
+
+    @NeverInline
+    public static void callCallApi() {
+      new ApiCaller().callApi();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      ApiCallerCaller.callCallApi();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java
new file mode 100644
index 0000000..f210e6f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInstanceFieldTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForField;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+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 ApiModelNoInliningOfHigherApiLevelInstanceFieldTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelInstanceFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Field apiField = Api.class.getDeclaredField("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("getInstanceField");
+    Method apiCallerCaller = ApiCallerCaller.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForField(apiField, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        // TODO(b/138781768): Should not be inlined
+        .inspect(verifyThat(parameters, apiCaller).inlinedInto(apiCallerCaller))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello World!");
+  }
+
+  // The api class does not have an api level to ensure it is not the instance initializer that is
+  // keeping us from inlining.
+  public static class Api {
+
+    public String apiLevel22 = "Hello World!";
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    public static void getInstanceField() {
+      System.out.println(new Api().apiLevel22);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCallerCaller {
+
+    @NeverInline
+    public static void noApiCall() {
+      ApiCaller.getInstanceField();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      ApiCallerCaller.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticFieldTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticFieldTest.java
new file mode 100644
index 0000000..0087275
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticFieldTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForField;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+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 ApiModelNoInliningOfHigherApiLevelStaticFieldTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelStaticFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Field apiField = Api.class.getDeclaredField("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("getStaticField");
+    Method apiCallerCaller = ApiCallerCaller.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForField(apiField, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        // TODO(b/138781768): Should not be inlined
+        .inspect(verifyThat(parameters, apiCaller).inlinedInto(apiCallerCaller))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello World!");
+  }
+
+  // The api class does not have an api level to ensure it is not the class reference is keeping us
+  // from inlining.
+  public static class Api {
+
+    public static String apiLevel22 = "Hello World!";
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    public static void getStaticField() {
+      System.out.println(Api.apiLevel22);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCallerCaller {
+
+    @NeverInline
+    public static void noApiCall() {
+      ApiCaller.getStaticField();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      ApiCallerCaller.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelVerticalMergingOfSuperClassTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelVerticalMergingOfSuperClassTest.java
new file mode 100644
index 0000000..a04774b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelVerticalMergingOfSuperClassTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForType;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.utils.AndroidApiLevel;
+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 ApiModelVerticalMergingOfSuperClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelVerticalMergingOfSuperClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .apply(setMockApiLevelForType(Api.class, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            inspector -> {
+              // TODO(b/138781768): We should not merge A into B.
+              assertThat(inspector.clazz(A.class), not(isPresent()));
+            })
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello World!");
+  }
+
+  public static class Api {}
+
+  public static class A extends Api {
+
+    @NeverInline
+    public void bar() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  @NeverClassInline
+  public static class B extends A {
+
+    public void foo() {
+      bar();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new B().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
index 5b1a2f7..b20e395 100644
--- a/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.CodeMatchers;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 
 public abstract class ApiModelingTestHelper {
@@ -34,6 +35,32 @@
     };
   }
 
+  static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
+      ThrowableConsumer<T> setMockApiLevelForField(Field field, AndroidApiLevel apiLevel) {
+    return compilerBuilder -> {
+      compilerBuilder.addOptionsModification(
+          options -> {
+            options
+                .apiModelingOptions()
+                .fieldApiMapping
+                .put(Reference.fieldFromField(field), apiLevel);
+          });
+    };
+  }
+
+  static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>> ThrowableConsumer<T> setMockApiLevelForType(
+      Class<?> clazz, AndroidApiLevel apiLevel) {
+    return compilerBuilder -> {
+      compilerBuilder.addOptionsModification(
+          options -> {
+            options
+                .apiModelingOptions()
+                .typeApiMapping
+                .put(Reference.classFromClass(clazz), apiLevel);
+          });
+    };
+  }
+
   static void enableApiCallerIdentification(TestCompilerBuilder<?, ?, ?, ?, ?> compilerBuilder) {
     compilerBuilder.addOptionsModification(
         options -> {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java
new file mode 100644
index 0000000..57108ff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest
+    extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdAfterUnusedArgumentRemovalMergingTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C", "D");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new Object(), new C()));
+      System.out.println(new B(new Object(), new D()));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+
+    A(Object unused, C c) {
+      this.c = c;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+
+    B(Object unused, D d) {
+      this.d = d;
+    }
+
+    @Override
+    public String toString() {
+      return d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentAndAssignmentOrderMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentAndAssignmentOrderMergingTest.java
new file mode 100644
index 0000000..e70cd5a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentAndAssignmentOrderMergingTest.java
@@ -0,0 +1,125 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdAndDifferentArgumentAndAssignmentOrderMergingTest
+    extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdAndDifferentArgumentAndAssignmentOrderMergingTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              // TODO(b/189296638): Enable constructor merging by changing the constructor
+              // arguments.
+              assertEquals(
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("CD", "CD");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new C(), new D()));
+      System.out.println(new B(new D(), new C()));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+    private final D d;
+
+    A(C c, D d) {
+      this.c = c;
+      this.d = d;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString() + d.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final C c;
+    private final D d;
+
+    B(D d, C c) {
+      this.d = d;
+      this.c = c;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString() + d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java
new file mode 100644
index 0000000..5297a09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest
+    extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdAndDifferentArgumentOrderMergingTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+
+              // TODO(b/189296638): Enable constructor merging by changing the constructor
+              // arguments.
+              assertEquals(
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C0", "D1");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(0, new C()));
+      System.out.println(new B(new D(), 1));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+    private final int i;
+
+    A(int i, C c) {
+      this.c = c;
+      this.i = i;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString() + i;
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+    private final int i;
+
+    B(D d, int i) {
+      this.d = d;
+      this.i = i;
+    }
+
+    @Override
+    public String toString() {
+      return d.toString() + i;
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java
new file mode 100644
index 0000000..7a7a997
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest
+    extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("CC", "DD");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new C(), new C()));
+      System.out.println(new B(new D(), new D()));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+    private final C c2;
+
+    A(C c, C c2) {
+      this.c = c;
+      this.c2 = c2;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString() + c2.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+    private final D d2;
+
+    B(D d, D d2) {
+      // d2 intentionally assigned before d.
+      this.d2 = d2;
+      this.d = d;
+    }
+
+    @Override
+    public String toString() {
+      return d.toString() + d2.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java
new file mode 100644
index 0000000..3917d56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdAndExtraNullsMergingTest.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdAndExtraNullsMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdAndExtraNullsMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+
+              ClassSubject nullArgumentClassSubject =
+                  inspector.allClasses().stream()
+                      .filter(
+                          clazz ->
+                              SyntheticItemsTestUtils.isHorizontalInitializerTypeArgument(
+                                  clazz.getOriginalReference()))
+                      .findFirst()
+                      .orElseThrow(RuntimeException::new);
+
+              assertThat(
+                  aClassSubject.method("void", "<init>", "java.lang.Object", "int"), isPresent());
+              assertThat(
+                  aClassSubject.method(
+                      "void",
+                      "<init>",
+                      "java.lang.Object",
+                      "int",
+                      nullArgumentClassSubject.getFinalName()),
+                  isPresent());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C", "42", "C", "D");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new C()));
+      System.out.println(new A(new C(), 42));
+      System.out.println(new B(new D()));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final Object c;
+
+    A(C c) {
+      this.c = c;
+    }
+
+    A(Object c, int i) {
+      this.c = c;
+      System.out.println(i);
+    }
+
+    @Override
+    public String toString() {
+      return c.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+
+    B(D d) {
+      this.d = d;
+    }
+
+    @Override
+    public String toString() {
+      return d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java
new file mode 100644
index 0000000..2bbd4e5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithClassIdMergingTest.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithClassIdMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithClassIdMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C", "D");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new C()));
+      System.out.println(new B(new D()));
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+
+    A(C c) {
+      this.c = c;
+    }
+
+    @Override
+    public String toString() {
+      return c.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+
+    B(D d) {
+      this.d = d;
+    }
+
+    @Override
+    public String toString() {
+      return d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java
new file mode 100644
index 0000000..c45274f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithInterfaceValueToParentTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 EquivalentConstructorsWithInterfaceValueToParentTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public EquivalentConstructorsWithInterfaceValueToParentTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("K", "L");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A(new KImpl());
+      new B(new LImpl());
+    }
+  }
+
+  static class Parent {
+    Parent(J j) {
+      j.m();
+    }
+  }
+
+  @NeverClassInline
+  static class A extends Parent {
+    A(K k) {
+      super(k);
+    }
+  }
+
+  @NeverClassInline
+  static class B extends Parent {
+    B(L l) {
+      super(l);
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class KImpl implements K {
+    @Override
+    public void m() {
+      System.out.println("K");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class LImpl implements L {
+    @Override
+    public void m() {
+      System.out.println("L");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {
+    void m();
+  }
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J extends I {}
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface K extends J {}
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface L extends J {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java
new file mode 100644
index 0000000..48e6535
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithPrimitiveAndReferencesParametersTest.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.KeepUnusedArguments;
+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.horizontal.EquivalentConstructorsWithClassIdAndDifferentAssignmentOrderMergingTest.A;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithPrimitiveAndReferencesParametersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public EquivalentConstructorsWithPrimitiveAndReferencesParametersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableUnusedArgumentAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  2, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A(42).m1();
+      new B(new Object()).m2();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+    @KeepUnusedArguments
+    A(int i) {}
+
+    @NeverInline
+    void m1() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+    @KeepUnusedArguments
+    B(Object o) {}
+
+    @NeverInline
+    void m2() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java
new file mode 100644
index 0000000..4a66189
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest.java
@@ -0,0 +1,121 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest
+    extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithoutClassIdAfterUnusedArgumentRemovalMergingTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C", "D");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new Object(), new C()).foo());
+      System.out.println(new B(new Object(), new D()).bar());
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+
+    A(Object unused, C c) {
+      this.c = c;
+    }
+
+    @NeverInline
+    public String foo() {
+      return c.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+
+    B(Object unused, D d) {
+      this.d = d;
+    }
+
+    @NeverInline
+    public String bar() {
+      return d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java
new file mode 100644
index 0000000..090699b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EquivalentConstructorsWithoutClassIdMergingTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+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 EquivalentConstructorsWithoutClassIdMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EquivalentConstructorsWithoutClassIdMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertEquals(
+                  1, aClassSubject.allMethods(FoundMethodSubject::isInstanceInitializer).size());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("C", "D");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A(new C()).foo());
+      System.out.println(new B(new D()).bar());
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private final C c;
+
+    A(C c) {
+      this.c = c;
+    }
+
+    @NeverInline
+    public String foo() {
+      return c.toString();
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    private final D d;
+
+    B(D d) {
+      this.d = d;
+    }
+
+    @NeverInline
+    public String bar() {
+      return d.toString();
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class C {
+
+    @Override
+    public String toString() {
+      return "C";
+    }
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  static class D {
+
+    @Override
+    public String toString() {
+      return "D";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/IllegalInliningOfMergedConstructorTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/IllegalInliningOfMergedConstructorTest.java
index bbc415c..aad40d0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/IllegalInliningOfMergedConstructorTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/IllegalInliningOfMergedConstructorTest.java
@@ -33,8 +33,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        .addEnumUnboxingInspector(
-            inspector -> inspector.assertUnboxedIf(parameters.isDexRuntime(), Reprocess.class))
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(Reprocess.class))
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertMergedInto(B.class, A.class))
         .addOptionsModification(options -> options.inliningInstructionLimit = 4)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
index e785238..c39b7b5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
@@ -44,6 +44,9 @@
         .addKeepMainRule(Main.class)
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -52,22 +55,22 @@
         .inspectStackTrace(
             (stackTrace, codeInspector) -> {
               assertThat(codeInspector.clazz(A.class), isPresent());
-                StackTrace expectedStackTraceWithMergedConstructor =
-                    StackTrace.builder()
-                        .add(expectedStackTrace)
-                        .add(
-                            2,
-                            StackTraceLine.builder()
-                                .setClassName(A.class.getTypeName())
-                                // TODO(b/124483578): The synthetic method should not be part of the
-                                //  retraced stack trace.
-                                .setMethodName("$r8$init$bridge")
-                                .setFileName(getClass().getSimpleName() + ".java")
-                                .setLineNumber(0)
-                                .build())
-                        .build();
-                assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
-                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+              StackTrace expectedStackTraceWithMergedConstructor =
+                  StackTrace.builder()
+                      .add(expectedStackTrace)
+                      .add(
+                          2,
+                          StackTraceLine.builder()
+                              .setClassName(A.class.getTypeName())
+                              // TODO(b/124483578): The synthetic method should not be part of the
+                              //  retraced stack trace.
+                              .setMethodName("$r8$init$synthetic")
+                              .setFileName(getClass().getSimpleName() + ".java")
+                              .setLineNumber(0)
+                              .build())
+                      .build();
+              assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
+              assertThat(codeInspector.clazz(B.class), not(isPresent()));
             });
   }
 
@@ -82,12 +85,18 @@
 
   @NeverClassInline
   static class A extends Parent {
-    A() {}
+    A() {
+      // To avoid constructor equivalence.
+      System.out.println("A");
+    }
   }
 
   @NeverClassInline
   static class B extends Parent {
-    B() {}
+    B() {
+      // To avoid constructor equivalence.
+      System.out.println("B");
+    }
   }
 
   static class Main {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java
new file mode 100644
index 0000000..9b5b87d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorWithEquivalenceStackTraceTest.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.classmerging.horizontal.MergedConstructorStackTraceTest.A;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MergedConstructorWithEquivalenceStackTraceTest extends HorizontalClassMergingTestBase {
+
+  public StackTrace expectedStackTrace;
+
+  public MergedConstructorWithEquivalenceStackTraceTest(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(CfRuntime.getSystemRuntime(), Main.class)
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector.assertIsCompleteMergeGroup(A.class, B.class).assertNoOtherClassesMerged())
+        .enableNoVerticalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .inspectStackTrace(
+            (stackTrace, codeInspector) -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              StackTrace expectedStackTraceWithMergedConstructor =
+                  StackTrace.builder()
+                      .add(expectedStackTrace)
+                      // TODO(b/124483578): Stack trace lines from the merging of equivalent
+                      //  constructors should retrace to the set of lines from each of the
+                      //  individual source constructors.
+                      .map(1, stackTraceLine -> stackTraceLine.builderOf().setLineNumber(0).build())
+                      // TODO(b/124483578): The synthetic method should not be part of the retraced
+                      //  stack trace.
+                      .add(
+                          2,
+                          StackTraceLine.builder()
+                              .setClassName(A.class.getTypeName())
+                              .setMethodName("$r8$init$synthetic")
+                              .setFileName(getClass().getSimpleName() + ".java")
+                              .setLineNumber(0)
+                              .build())
+                      .build();
+              assertThat(stackTrace, isSame(expectedStackTraceWithMergedConstructor));
+              assertThat(codeInspector.clazz(B.class), not(isPresent()));
+            });
+  }
+
+  @NoVerticalClassMerging
+  static class Parent {
+    Parent() {
+      if (System.currentTimeMillis() >= 0) {
+        throw new RuntimeException();
+      }
+    }
+  }
+
+  @NeverClassInline
+  static class A extends Parent {
+    A() {}
+  }
+
+  @NeverClassInline
+  static class B extends Parent {
+    B() {}
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      new A();
+      new B();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/MissingBridgeTest.java b/src/test/java/com/android/tools/r8/desugar/MissingBridgeTest.java
new file mode 100644
index 0000000..e02104d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/MissingBridgeTest.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MissingBridgeTest extends TestBase {
+
+  static final String EXPECTED_WITH_BRIDGE = StringUtils.lines("null", "non-null");
+  static final String EXPECTED_WITHOUT_BRIDGE = StringUtils.lines("null", "null");
+
+  private final TestParameters parameters;
+  private final boolean withBridge;
+
+  @Parameterized.Parameters(name = "{0}, bridge:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values());
+  }
+
+  public MissingBridgeTest(TestParameters parameters, boolean withBridge) {
+    this.parameters = parameters;
+    this.withBridge = withBridge;
+  }
+
+  private String getExpected() {
+    return withBridge ? EXPECTED_WITH_BRIDGE : EXPECTED_WITHOUT_BRIDGE;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForDesugaring(parameters)
+        .addProgramClasses(TestClass.class, I.class, A.class, B.class)
+        .addProgramClassFileData(getTransformedJ())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(getExpected());
+  }
+
+  private byte[] getTransformedJ() throws IOException {
+    return transformer(J.class)
+        .removeMethods(
+            (access, name, descriptor, signature, exceptions) -> {
+              if (!withBridge && MethodAccessFlags.fromCfAccessFlags(access, false).isBridge()) {
+                assertEquals("doIt", name);
+                assertEquals("()Ljava/lang/Object;", descriptor);
+                return true;
+              }
+              return false;
+            })
+        .transform();
+  }
+
+  interface I<T> {
+    default T doIt() {
+      return null;
+    }
+  }
+
+  interface J extends I<String> {
+
+    // If withBridge==true the javac bridge is kept, otherwise stripped:
+    // default Object doIt() { return invoke-virtual {this} J.doIt()Ljava/lang/String; }
+
+    @Override
+    default String doIt() {
+      return "non-null";
+    }
+  }
+
+  static class A implements I {}
+
+  static class B implements J {}
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(((I) new A()).doIt());
+      System.out.println(((I) new B()).doIt());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
index fdb7faa..a4c52ad 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
@@ -115,15 +115,11 @@
     private List<Path> additionalProgramFiles = new ArrayList<>();
     private Consumer<InternalOptions> optionsModifier = ConsumerUtils.emptyConsumer();
     private Path desugarJDKLibs = ToolHelper.getDesugarJDKLibs();
-    private Path desugarJDKLibsConfiguration = ToolHelper.DESUGAR_LIB_CONVERSIONS;
+    private Path desugarJDKLibsConfiguration = null;
     private StringResource desugaredLibraryConfiguration =
         StringResource.fromFile(ToolHelper.getDesugarLibJsonForTesting());
     private List<Path> libraryFiles = new ArrayList<>();
 
-    public static L8TestBuilder builder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
-      return new L8TestBuilder(apiLevel, temp);
-    }
-
     private L8TestBuilder(AndroidApiLevel apiLevel, TemporaryFolder temp) {
       this.apiLevel = apiLevel;
       this.state = new TestState(temp);
@@ -239,11 +235,11 @@
     }
 
     private Collection<Path> getProgramFiles() {
-      return ImmutableList.<Path>builder()
-          .add(desugarJDKLibs)
-          .add(desugarJDKLibsConfiguration)
-          .addAll(additionalProgramFiles)
-          .build();
+      ImmutableList.Builder<Path> builder = ImmutableList.<Path>builder().add(desugarJDKLibs);
+      if (desugarJDKLibsConfiguration != null) {
+        builder.add(desugarJDKLibsConfiguration);
+      }
+      return builder.addAll(additionalProgramFiles).build();
     }
 
     private Collection<Path> getLibraryFiles() {
@@ -300,6 +296,7 @@
               },
               L8TestBuilder::setDebug)
           .addOptionsModifier(optionsModifier)
+          .setDesugarJDKLibsConfiguration(ToolHelper.DESUGAR_LIB_CONVERSIONS)
           // If we compile extended library here, it means we use TestNG. TestNG requires
           // annotations, hence we disable annotation removal. This implies that extra warnings are
           // generated.
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/MethodParametersTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/MethodParametersTest.java
new file mode 100644
index 0000000..8bec281
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/MethodParametersTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.nestaccesscontrol;
+
+import static com.android.tools.r8.TestRuntime.getCheckedInJdk11;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+
+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.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugar.nestaccesscontrol.methodparameters.Outer;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MethodParametersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // java.lang.reflect.Method.getParameters() supported from Android 8.1.
+    return getTestParameters()
+        .withDexRuntimesStartingFromIncluding(Version.V8_1_0)
+        .withCfRuntimes()
+        .withAllApiLevelsAlsoForCf()
+        .build();
+  }
+
+  public MethodParametersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    // Compile with javac from JDK 11 to get a class file using nest access control.
+    Path nestCompiledWithParameters =
+        javac(getCheckedInJdk11())
+            .addSourceFiles(ToolHelper.getSourceFileForTestClass(Outer.class))
+            .addOptions("-parameters")
+            .compile();
+
+    Path nestDesugared =
+        testForD8(Backend.CF)
+            .addProgramFiles(nestCompiledWithParameters)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    Path nestDesugaredTwice =
+        testForD8(Backend.CF)
+            .addProgramFiles(nestDesugared)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            // TODO(b/189743726): These warnings should not be there.
+            .assertAtLeastOneInfoMessage()
+            .assertAllInfoMessagesMatch(
+                anyOf(
+                    containsString("Invalid parameter counts in MethodParameter attributes"),
+                    containsString("Methods with invalid MethodParameter attributes")))
+            .writeToZip();
+
+    Path programDesugared =
+        testForD8(Backend.CF)
+            .addClasspathClasses(Outer.class)
+            .addInnerClasses(getClass())
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    testForD8(parameters.getBackend())
+        .addProgramFiles(nestDesugaredTwice)
+        .addProgramFiles(programDesugared)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        // TODO(b/189743726): These warnings should not be there.
+        .assertAtLeastOneInfoMessage()
+        .assertAllInfoMessagesMatch(
+            anyOf(
+                containsString("Invalid parameter counts in MethodParameter attributes"),
+                containsString("Methods with invalid MethodParameter attributes")))
+        .run(parameters.getRuntime(), TestRunner.class)
+        // TODO(b/189743726): Should not fail at runtime.
+        .assertFailureWithErrorThatMatches(
+            containsString("Wrong number of parameters in MethodParameters attribute"));
+  }
+
+  static class TestRunner {
+
+    public static void main(String[] args) {
+      Outer.callPrivateInnerConstructorZeroArgs();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/methodparameters/Outer.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/methodparameters/Outer.java
new file mode 100644
index 0000000..9c92994
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/methodparameters/Outer.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.nestaccesscontrol.methodparameters;
+
+import java.lang.reflect.Constructor;
+
+public class Outer {
+  public static class Inner {
+    private Inner() {
+      for (Constructor<?> constructor : getClass().getDeclaredConstructors()) {
+        System.out.println(constructor.getParameters().length);
+      }
+    }
+
+    private Inner(int a) {}
+
+    private Inner(int a, int b) {}
+  }
+
+  public static Inner callPrivateInnerConstructorZeroArgs() {
+    return new Inner();
+  }
+
+  public static Inner callPrivateInnerConstructorOneArg() {
+    return new Inner(0);
+  }
+
+  public static Inner callPrivateInnerConstructorTwoArgs() {
+    return new Inner(0, 0);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/MethodParametersTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/MethodParametersTest.java
new file mode 100644
index 0000000..7d70d21
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/MethodParametersTest.java
@@ -0,0 +1,141 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugaring.interfacemethods;
+
+import static com.android.tools.r8.TestRuntime.getCheckedInJdk;
+import static com.android.tools.r8.TestRuntime.getCheckedInJdk11;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugaring.interfacemethods.methodparameters.I;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MethodParametersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // java.lang.reflect.Method.getParameters() supported from Android 8.1.
+    return getTestParameters()
+        .withDexRuntimesStartingFromIncluding(Version.V8_1_0)
+        .withCfRuntimes()
+        .withAllApiLevelsAlsoForCf()
+        .build();
+  }
+
+  public MethodParametersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClassesAndInnerClasses(I.class)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestRunner.class)
+        .assertSuccessWithOutputLines("0", "1", "2", "0", "1", "2");
+  }
+
+  @Test
+  public void testDesugar() throws Exception {
+    // JDK8 is not present on Windows.
+    assumeTrue(
+        parameters.isDexRuntime()
+            || getCheckedInJdk(parameters.getRuntime().asCf().getVm()) != null);
+    Path compiledWithParameters =
+        javac(
+                parameters.isCfRuntime()
+                    ? getCheckedInJdk(parameters.getRuntime().asCf().getVm())
+                    : getCheckedInJdk11())
+            .addSourceFiles(ToolHelper.getSourceFileForTestClass(I.class))
+            .addOptions("-parameters")
+            .compile();
+
+    Path interfaceDesugared =
+        testForD8(Backend.CF)
+            .addProgramFiles(compiledWithParameters)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    Path interfaceDesugaredTwice =
+        testForD8(Backend.CF)
+            .addProgramFiles(interfaceDesugared)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            // TODO(b/189743726): These warnings should not be there.
+            .applyIf(
+                parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring(),
+                TestCompileResult::assertNoInfoMessages,
+                r ->
+                    r.assertAtLeastOneInfoMessage()
+                        .assertAllInfoMessagesMatch(
+                            anyOf(
+                                containsString(
+                                    "Invalid parameter counts in MethodParameter attributes"),
+                                containsString("Methods with invalid MethodParameter attributes"))))
+            .writeToZip();
+
+    Path programDesugared =
+        testForD8(Backend.CF)
+            .addClasspathClasses(I.class)
+            .addInnerClasses(getClass())
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    testForD8(parameters.getBackend())
+        .addProgramFiles(interfaceDesugaredTwice)
+        .addProgramFiles(programDesugared)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        // TODO(b/189743726): These warnings should not be there.
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring(),
+            TestCompileResult::assertNoInfoMessages,
+            r ->
+                r.assertAtLeastOneInfoMessage()
+                    .assertAllInfoMessagesMatch(
+                        anyOf(
+                            containsString(
+                                "Invalid parameter counts in MethodParameter attributes"),
+                            containsString("Methods with invalid MethodParameter attributes"))))
+        .run(parameters.getRuntime(), TestRunner.class)
+        .applyIf(
+            parameters.canUseDefaultAndStaticInterfaceMethodsWhenDesugaring(),
+            r -> r.assertSuccessWithOutputLines("0", "1", "2", "0", "1", "2"),
+            // TODO(b/189743726): Should not fail at runtime (but will have different parameter
+            // count for non-static methods when desugared).
+            r ->
+                r.assertFailureWithErrorThatMatches(
+                    containsString("Wrong number of parameters in MethodParameters attribute")));
+  }
+
+  static class A implements I {}
+
+  static class TestRunner {
+
+    public static void main(String[] args) {
+      new A().zeroArgsDefault();
+      new A().oneArgDefault(0);
+      new A().twoArgDefault(0, 0);
+      I.zeroArgStatic();
+      I.oneArgStatic(0);
+      I.twoArgsStatic(0, 0);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/methodparameters/I.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/methodparameters/I.java
new file mode 100644
index 0000000..4c7ba69
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/methodparameters/I.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugaring.interfacemethods.methodparameters;
+
+public interface I {
+
+  default void zeroArgsDefault() {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+
+  default void oneArgDefault(int a) {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+
+  default void twoArgDefault(int a, int b) {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+
+  static void zeroArgStatic() {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+
+  static void oneArgStatic(int a) {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+
+  static void twoArgsStatic(int a, int b) {
+    System.out.println(new Object() {}.getClass().getEnclosingMethod().getParameters().length);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
index a8fe3f9..2a8105f 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
@@ -85,11 +85,6 @@
         .addKeepRules("-keep enum * { <fields>; }")
         .addKeepRules(enumKeepRules.getKeepRules())
         .addKeepRules(missingStaticMethods ? "" : "-keep enum * { static <methods>; }")
-        .addOptionsModification(
-            options -> {
-              assert !options.enableEnumUnboxing;
-              options.enableEnumUnboxing = true;
-            })
         .addKeepClassRules(Lib.LibEnumStaticMethod.class)
         .addEnumUnboxingInspector(
             inspector ->
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java
index aba1bb8..b99986a 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java
@@ -40,14 +40,6 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addKeepRules(enumKeepRules.getKeepRules())
-        .addOptionsModification(
-            options -> {
-              if (options.isGeneratingClassFiles()) {
-                // TODO(b/172568606): Remove this when enabled for CF by default.
-                assertFalse(options.enableEnumUnboxing);
-                options.enableEnumUnboxing = true;
-              }
-            })
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureAllowShrinkingTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureAllowShrinkingTest.java
new file mode 100644
index 0000000..be24a34
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureAllowShrinkingTest.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.lang.reflect.ParameterizedType;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+// This is a reproduction of b/189443104.
+public class GenericSignatureAllowShrinkingTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"class java.lang.String"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignatureAllowShrinkingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8WithDirectKeep() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassRules(Foo.class)
+        .addKeepRules("-keep class * extends " + Foo.class.getTypeName() + " { *; }")
+        .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8AllowShrinking() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules("-keep,allowshrinking class " + Foo.class.getTypeName() + " { *; }")
+        .addKeepRules(
+            "-keep,allowshrinking,allowobfuscation class * extends "
+                + Foo.class.getTypeName()
+                + " { *; }")
+        .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
+        .addKeepMainRule(Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject foo = inspector.clazz(Foo.class);
+    assertThat(foo, isPresent());
+    assertEquals("<T:Ljava/lang/Object;>Ljava/lang/Object;", foo.getFinalSignatureAttribute());
+
+    ClassSubject main$1 = inspector.clazz(Main.class.getTypeName() + "$1");
+    assertThat(main$1, isPresent());
+    assertEquals(
+        "L" + binaryName(Foo.class) + "<Ljava/lang/String;>;", main$1.getFinalSignatureAttribute());
+  }
+
+  public static class Foo<T> {
+
+    public void print() {
+      ParameterizedType genericSuperclass =
+          (ParameterizedType) this.getClass().getGenericSuperclass();
+      System.out.println(genericSuperclass.getActualTypeArguments()[0].toString());
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      (new Foo<String>() {}).print();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureInvalidInfoKeepTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureInvalidInfoKeepTest.java
new file mode 100644
index 0000000..b9248aa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureInvalidInfoKeepTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 GenericSignatureInvalidInfoKeepTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignatureInvalidInfoKeepTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Base.class, Main.class)
+        .addInnerClasses(A.class)
+        .addProgramClassFileData(
+            transformer(A.class)
+                .setGenericSignature(MethodPredicate.onName("foo"), (String) null)
+                .transform())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .compile()
+        // This tests that no info messages are generated due to us removing the generic parameters
+        // K and V from A::base.
+        .assertNoInfoMessages()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A$1::bar")
+        .inspect(
+            inspector -> {
+              ClassSubject baseClass = inspector.clazz(Base.class);
+              assertThat(baseClass, not(isPresent()));
+            });
+  }
+
+  public abstract static class Base<K, V> {
+    public abstract K bar(V v);
+  }
+
+  public static class A {
+
+    public static <K, V> Base<K, V> foo() {
+      return new Base<K, V>() {
+        @Override
+        public K bar(V f) {
+          System.out.println("A$1::bar");
+          return null;
+        }
+      };
+    }
+    ;
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.foo().bar(null);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java b/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java
new file mode 100644
index 0000000..c1182bd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeV1620Test.java
@@ -0,0 +1,191 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.internal;
+
+import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static com.android.tools.r8.ToolHelper.shouldRunSlowTests;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.assertRewrittenProtoSchemasMatch;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepAllProtosRule;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepDynamicMethodSignatureRule;
+import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepNewMessageInfoSignatureRule;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.L8TestCompileResult;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutionException;
+import org.junit.After;
+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 YouTubeV1620Test extends YouTubeCompilationTestBase {
+
+  private static final int MAX_APPLICATION_SIZE = 29750000;
+  private static final int MAX_DESUGARED_LIBRARY_SIZE = 425000;
+
+  private final TestParameters parameters;
+
+  private final Path dumpDirectory = Paths.get("YouTubeV1620-" + System.currentTimeMillis());
+  private final Reporter reporter = new Reporter();
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntime(Version.V9_0_0)
+        .withApiLevel(AndroidApiLevel.L)
+        .build();
+  }
+
+  public YouTubeV1620Test(TestParameters parameters) {
+    super(16, 20, parameters.getApiLevel());
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    assumeTrue(isLocalDevelopment());
+    assumeTrue(shouldRunSlowTests());
+
+    KeepRuleConsumer keepRuleConsumer = new PresentKeepRuleConsumer();
+    R8TestCompileResult r8CompileResult = compileApplicationWithR8(keepRuleConsumer);
+    L8TestCompileResult l8CompileResult = compileDesugaredLibraryWithL8(keepRuleConsumer);
+
+    inspect(r8CompileResult, l8CompileResult);
+
+    if (isLocalDevelopment()) {
+      dump(r8CompileResult, l8CompileResult);
+    }
+  }
+
+  @Test
+  public void testProtoRewriting() throws Exception {
+    assumeTrue(shouldRunSlowTests());
+
+    StringConsumer keepRuleConsumer = StringConsumer.emptyConsumer();
+    R8TestCompileResult r8CompileResult =
+        compileApplicationWithR8(
+            keepRuleConsumer,
+            builder ->
+                builder
+                    .addKeepRules(
+                        keepAllProtosRule(),
+                        keepDynamicMethodSignatureRule(),
+                        keepNewMessageInfoSignatureRule())
+                    .allowCheckDiscardedErrors(true));
+    assertRewrittenProtoSchemasMatch(
+        new CodeInspector(getProgramFiles()), r8CompileResult.inspector());
+  }
+
+  @After
+  public void teardown() {
+    reporter.failIfPendingErrors();
+  }
+
+  private R8TestCompileResult compileApplicationWithR8(StringConsumer keepRuleConsumer)
+      throws IOException, CompilationFailedException {
+    return compileApplicationWithR8(keepRuleConsumer, ThrowableConsumer.empty());
+  }
+
+  private R8TestCompileResult compileApplicationWithR8(
+      StringConsumer keepRuleConsumer, ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws IOException, CompilationFailedException {
+    return testForR8(parameters.getBackend())
+        .addProgramFiles(getProgramFiles())
+        .addLibraryFiles(getLibraryFiles())
+        .addKeepRuleFiles(getKeepRuleFiles())
+        .addDontWarn("android.app.Activity$TranslucentConversionListener")
+        .allowDiagnosticMessages()
+        .allowUnusedDontWarnPatterns()
+        .allowUnusedProguardConfigurationRules()
+        .apply(configuration)
+        .setMinApi(getApiLevel())
+        .enableCoreLibraryDesugaring(
+            getApiLevel(),
+            keepRuleConsumer,
+            StringResource.fromFile(getDesugaredLibraryConfiguration()))
+        .compile()
+        .assertAllInfoMessagesMatch(
+            anyOf(
+                containsString("Ignoring option: -optimizations"),
+                containsString("Proguard configuration rule does not match anything"),
+                containsString("Invalid signature")))
+        .apply(this::printProtoStats);
+  }
+
+  private L8TestCompileResult compileDesugaredLibraryWithL8(KeepRuleConsumer keepRuleConsumer)
+      throws CompilationFailedException, IOException, ExecutionException {
+    return testForL8(getApiLevel())
+        .setDesugaredLibraryConfiguration(getDesugaredLibraryConfiguration())
+        .setDesugarJDKLibs(getDesugaredLibraryJDKLibs())
+        .addGeneratedKeepRules(keepRuleConsumer.get())
+        .addKeepRuleFiles(getDesugaredLibraryKeepRuleFiles())
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+        .compile();
+  }
+
+  private void inspect(R8TestCompileResult r8CompileResult, L8TestCompileResult l8CompileResult)
+      throws IOException, ResourceException {
+    r8CompileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+    l8CompileResult.runDex2Oat(parameters.getRuntime()).assertNoVerificationErrors();
+
+    int applicationSize = r8CompileResult.getApp().applicationSize();
+    if (applicationSize > MAX_APPLICATION_SIZE) {
+      reporter.error(
+          "Expected application size to be <="
+              + MAX_APPLICATION_SIZE
+              + ", but was "
+              + applicationSize);
+    }
+
+    int desugaredLibrarySize = l8CompileResult.getApp().applicationSize();
+    if (desugaredLibrarySize > MAX_DESUGARED_LIBRARY_SIZE) {
+      reporter.error(
+          "Expected desugared library size to be <="
+              + MAX_DESUGARED_LIBRARY_SIZE
+              + ", but was "
+              + desugaredLibrarySize);
+    }
+
+    if (isLocalDevelopment()) {
+      System.out.println("Dex size (application, excluding desugared library): " + applicationSize);
+      System.out.println("Dex size (desugared library): " + desugaredLibrarySize);
+      System.out.println("Dex size (total): " + (applicationSize + desugaredLibrarySize));
+    }
+  }
+
+  private void dump(R8TestCompileResult r8CompileResult, L8TestCompileResult l8CompileResult)
+      throws IOException {
+    assertTrue(isLocalDevelopment());
+    Files.createDirectories(dumpDirectory);
+    r8CompileResult
+        .writeToDirectory(dumpDirectory)
+        .writeProguardMap(dumpDirectory.resolve("mapping.txt"));
+    l8CompileResult
+        .writeSingleDexOutputToFile(dumpDirectory.resolve("classes5.dex"))
+        .writeGeneratedKeepRules(dumpDirectory.resolve("l8-keep.txt"))
+        .writeProguardMap(dumpDirectory.resolve("l8-mapping.txt"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
index e5746ec..5dd3140 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -62,14 +61,15 @@
   public void testWriteOnlyField_dontoptimize() throws Exception {
     CodeInspector inspector = runR8(DONT_OPTIMIZE);
     ClassSubject clazz = inspector.clazz(QUALIFIED_CLASS_NAME);
-    // With the support of 'allowshrinking' dontoptimize will now effectively pin all
-    // items that are not tree shaken out. The field operations will thus remain.
-    assertTrue(clazz.clinit().streamInstructions().anyMatch(InstructionSubject::isStaticPut));
-    assertTrue(
-        clazz
-            .uniqueInstanceInitializer()
-            .streamInstructions()
-            .anyMatch(InstructionSubject::isInstancePut));
+    clazz.forAllMethods(
+        methodSubject -> {
+          // Dead code removal is not part of -dontoptimize. That is, even with -dontoptimize,
+          // field put instructions are gone with better dead code removal.
+          assertTrue(
+              methodSubject
+                  .streamInstructions()
+                  .noneMatch(i -> i.isInstancePut() || i.isStaticPut()));
+        });
   }
 
   private CodeInspector runR8(Path proguardConfig) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index aad2f6f..95c8527 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -24,7 +24,6 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -358,10 +357,8 @@
     ClassSubject clazz = inspector.clazz(nullabilityClass);
     assertThat(clazz.uniqueMethodWithName("conditionalOperator"), isAbsent());
 
-    // The enum parameter may get unboxed.
-    MethodSubject m =
-        clazz.uniqueMethodWithName(
-            parameters.isCfRuntime() ? "moreControlFlows" : "moreControlFlows$enumunboxing$");
+    // The enum parameter is unboxed.
+    MethodSubject m = clazz.uniqueMethodWithName("moreControlFlows$enumunboxing$");
     assertTrue(m.isPresent());
 
     // Verify that a.b() is resolved to an inline instance-get.
@@ -381,18 +378,10 @@
   }
 
   private boolean isEnumInvoke(InstructionSubject instruction) {
-    InternalOptions defaults = new InternalOptions();
-    if (parameters.isDexRuntime() && defaults.enableEnumUnboxing) {
-      return instruction
-          .getMethod()
-          .name
-          .toString()
-          .startsWith(EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_METHOD_PREFIX);
-    } else {
-      return ((InvokeInstructionSubject) instruction)
-          .holder()
-          .toString()
-          .contains("java.lang.Enum");
-    }
+    return instruction
+        .getMethod()
+        .getName()
+        .toString()
+        .startsWith(EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_METHOD_PREFIX);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/ConditionalSimpleInliningWithEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/ConditionalSimpleInliningWithEnumUnboxingTest.java
index 3cebb62..7eb5882 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/ConditionalSimpleInliningWithEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/conditionalsimpleinlining/ConditionalSimpleInliningWithEnumUnboxingTest.java
@@ -28,9 +28,7 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .apply(this::configure)
-        .addEnumUnboxingInspector(
-            inspector ->
-                inspector.assertUnboxedIf(parameters.isDexRuntime(), EnumUnboxingCandidate.class))
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(EnumUnboxingCandidate.class))
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/NoOutliningForClassFileBuildsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/NoOutliningForClassFileBuildsTest.java
deleted file mode 100644
index 2a837bc..0000000
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/NoOutliningForClassFileBuildsTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (c) 2019, 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.optimize.outliner;
-
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class NoOutliningForClassFileBuildsTest extends TestBase {
-
-  static final String EXPECTED = StringUtils.lines("foobarbaz");
-
-  private final TestParameters parameters;
-  private final boolean forceOutline;
-
-  @Parameterized.Parameters(name = "{0}, force-outline: {1}")
-  public static Collection<Object[]> data() {
-    List<Object[]> args = new ArrayList<>();
-    for (TestParameters parameter : getTestParameters().withAllRuntimesAndApiLevels().build()) {
-      args.add(new Object[] {parameter, false});
-      if (parameter.isCfRuntime()) {
-        args.add(new Object[] {parameter, true});
-      }
-    }
-    return args;
-  }
-
-  public NoOutliningForClassFileBuildsTest(TestParameters parameters, boolean forceOutline) {
-    this.parameters = parameters;
-    this.forceOutline = forceOutline;
-  }
-
-  @Test
-  public void test() throws Exception {
-    testForR8(parameters.getBackend())
-        .noMinification()
-        .addProgramClasses(TestClass.class)
-        .addKeepClassAndMembersRules(TestClass.class)
-        .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(
-            o -> {
-              if (forceOutline) {
-                o.outline.enabled = true;
-              }
-              o.outline.minSize = 2;
-              o.outline.maxSize = 20;
-              o.outline.threshold = 2;
-            })
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspect(this::checkOutlining);
-  }
-
-  private void checkOutlining(CodeInspector inspector) {
-    ClassSubject classSubject = inspector.clazz(TestClass.class);
-    assertThat(classSubject, isPresent());
-    assertThat(classSubject.uniqueMethodWithName("foo"), isPresent());
-    assertThat(classSubject.uniqueMethodWithName("bar"), isPresent());
-    boolean hasOutlineClass =
-        inspector
-            .clazz(SyntheticItemsTestUtils.syntheticOutlineClass(TestClass.class, 0))
-            .isPresent();
-    assertEquals(forceOutline || parameters.isDexRuntime(), hasOutlineClass);
-  }
-
-  static class TestClass {
-
-    public void foo(String arg) {
-      StringBuilder builder = new StringBuilder();
-      builder.append("foo");
-      builder.append(arg);
-      builder.append("baz");
-      System.out.println(builder.toString());
-    }
-
-    public void bar(String arg) {
-      StringBuilder builder = new StringBuilder();
-      builder.append("foo");
-      builder.append(arg);
-      builder.append("baz");
-      System.out.println(builder.toString());
-    }
-
-    public static void main(String[] args) {
-      new TestClass().foo("bar");
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
index 0531857..5660355 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
@@ -43,10 +43,6 @@
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlinesWithNonNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlinesWithNonNullTest.java
index 6b7bc4a..c4b84a7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlinesWithNonNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlinesWithNonNullTest.java
@@ -35,7 +35,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public OutlinesWithNonNullTest(TestParameters parameters) {
@@ -49,15 +49,11 @@
         .enableInliningAnnotations()
         .addProgramClasses(TestArg.class, TestClassWithNonNullOnOneSide.class)
         .addKeepMainRule(TestClassWithNonNullOnOneSide.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .allowAccessModification()
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
@@ -74,15 +70,11 @@
         .enableInliningAnnotations()
         .addProgramClasses(TestArg.class, TestClassWithNonNullOnBothSides.class)
         .addKeepMainRule(TestClassWithNonNullOnBothSides.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .allowAccessModification()
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithClassArrayTypeArguments.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithClassArrayTypeArguments.java
index 440b036..8bd05e0 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithClassArrayTypeArguments.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithClassArrayTypeArguments.java
@@ -29,7 +29,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public OutlinesWithClassArrayTypeArguments(TestParameters parameters) {
@@ -60,14 +60,10 @@
         .enableInliningAnnotations()
         .addInnerClasses(OutlinesWithClassArrayTypeArguments.class)
         .addKeepMainRule(TestClass.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithInterfaceArrayTypeArguments.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithInterfaceArrayTypeArguments.java
index 062516c..5a0b120 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithInterfaceArrayTypeArguments.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithInterfaceArrayTypeArguments.java
@@ -32,7 +32,8 @@
 
   @Parameterized.Parameters(name = "{1}, allow interface array types in outlining on JVM: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public OutlinesWithInterfaceArrayTypeArguments(
@@ -73,14 +74,10 @@
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(ClassImplementingIface.class)
         .addKeepClassAndMembersRules(OtherClassImplementingIface.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
               options.testing.allowOutlinerInterfaceArrayArguments =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithPrimitiveArrayTypeArguments.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithPrimitiveArrayTypeArguments.java
index 36d9ad5..6334006 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithPrimitiveArrayTypeArguments.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/arraytypes/OutlinesWithPrimitiveArrayTypeArguments.java
@@ -63,10 +63,6 @@
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
index 1e5ef3f..d5e8708 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
@@ -88,10 +88,6 @@
             .addKeepMainRule(TestClass.class)
             .addOptionsModification(
                 options -> {
-                  if (parameters.isCfRuntime()) {
-                    assert !options.outline.enabled;
-                    options.outline.enabled = true;
-                  }
                   // To trigger outliner, set # of expected outline candidate as threshold.
                   options.outline.threshold = 2;
                   options.enableInlining = false;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b133215941/B133215941.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b133215941/B133215941.java
index 031fd9d..9c8eb76 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b133215941/B133215941.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b133215941/B133215941.java
@@ -33,7 +33,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public B133215941(TestParameters parameters) {
@@ -69,16 +69,9 @@
         .addInnerClasses(B133215941.class)
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(ClassWithStaticMethod.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
-        .addOptionsModification(
-            options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
-              options.outline.threshold = 2;
-            })
+        .addOptionsModification(options -> options.outline.threshold = 2)
         .compile()
         .inspect(this::validateOutlining)
         .run(parameters.getRuntime(), TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
index 39c95f1..29de662 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/classtypes/B134462736.java
@@ -29,7 +29,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public B134462736(TestParameters parameters) {
@@ -64,14 +64,10 @@
         .enableInliningAnnotations()
         .addInnerClasses(B134462736.class)
         .addKeepMainRule(TestClass.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
index 4b81714..e384d9b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/primitivetypes/PrimitiveTypesTest.java
@@ -30,7 +30,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public PrimitiveTypesTest(TestParameters parameters) {
@@ -61,14 +61,10 @@
         .addProgramClasses(testClass)
         .addProgramClasses(MyStringBuilder.class)
         .addKeepMainRule(testClass)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .noMinification()
         .addOptionsModification(
             options -> {
-              if (parameters.isCfRuntime()) {
-                assert !options.outline.enabled;
-                options.outline.enabled = true;
-              }
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
index 049974c..548ce4d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -100,20 +100,12 @@
 
   @Test
   public void testDontOptimize() throws Exception {
-    test(
-        builder -> {
-          builder.allowDiagnosticInfoMessages();
-          builder.noOptimization();
-        });
+    test(TestShrinkerBuilder::noOptimization);
   }
 
   @Test
   public void testDontObfuscate() throws Exception {
-    test(
-        builder -> {
-          builder.allowDiagnosticInfoMessages();
-          builder.noMinification();
-        });
+    test(TestShrinkerBuilder::noMinification);
   }
 
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index e2c0a0a..ec3722c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -6,8 +6,10 @@
 
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -64,34 +66,40 @@
   public void test_dataclass_gettersOnly() throws Exception {
     // TODO(b/179866251): Allow for CF code.
     assumeTrue(testParameters.isDexRuntime());
-    final String mainClassName = "dataclass.MainGettersOnlyKt";
-    final MethodSignature testMethodSignature =
+    String mainClassName = "dataclass.MainGettersOnlyKt";
+    MethodSignature testMethodSignature =
         new MethodSignature("testDataClassGetters", "void", Collections.emptyList());
-    final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
     runTest(
             "dataclass",
             mainClassName,
             testBuilder ->
                 testBuilder
-                    .addKeepRules(extraRules)
+                    .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
-              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
+              if (allowAccessModification
+                  && kotlinParameters.is(
+                      KotlinCompilerVersion.KOTLINC_1_5_0_M2, KotlinTargetVersion.JAVA_8)) {
+                checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName());
+              } else {
+                ClassSubject dataClass =
+                    checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-              // Getters should be removed after inlining, which is possible only if access is
-              // relaxed.
-              final boolean areGetterPresent = !allowAccessModification;
-              checkMethodIsKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
-              checkMethodIsKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
+                // Getters should be removed after inlining, which is possible only if access is
+                // relaxed.
+                boolean areGetterPresent = !allowAccessModification;
+                checkMethodIsKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
+                checkMethodIsKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
 
-              // No use of componentN functions.
-              checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
-              checkMethodIsRemoved(dataClass, COMPONENT2_METHOD);
+                // No use of componentN functions.
+                checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
+                checkMethodIsRemoved(dataClass, COMPONENT2_METHOD);
 
-              // No use of copy functions.
-              checkMethodIsRemoved(dataClass, COPY_METHOD);
-              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+                // No use of copy functions.
+                checkMethodIsRemoved(dataClass, COPY_METHOD);
+                checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              }
 
               ClassSubject classSubject = checkClassIsKept(inspector, mainClassName);
               MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
@@ -109,34 +117,42 @@
   public void test_dataclass_componentOnly() throws Exception {
     // TODO(b/179866251): Allow for CF code.
     assumeTrue(testParameters.isDexRuntime());
-    final String mainClassName = "dataclass.MainComponentOnlyKt";
-    final MethodSignature testMethodSignature =
+    String mainClassName = "dataclass.MainComponentOnlyKt";
+    MethodSignature testMethodSignature =
         new MethodSignature("testAllDataClassComponentFunctions", "void", Collections.emptyList());
-    final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
     runTest(
             "dataclass",
             mainClassName,
             testBuilder ->
                 testBuilder
-                    .addKeepRules(extraRules)
+                    .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
-              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
+              if (allowAccessModification
+                  && kotlinParameters.is(
+                      KotlinCompilerVersion.KOTLINC_1_5_0_M2, KotlinTargetVersion.JAVA_8)) {
+                checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName());
+              } else {
+                ClassSubject dataClass =
+                    checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-              // ComponentN functions should be removed after inlining, which is possible only if
-              // access is relaxed.
-              final boolean areComponentMethodsPresent = !allowAccessModification;
-              checkMethodIsKeptOrRemoved(dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
-              checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
+                // ComponentN functions should be removed after inlining, which is possible only if
+                // access is relaxed.
+                boolean areComponentMethodsPresent = !allowAccessModification;
+                checkMethodIsKeptOrRemoved(
+                    dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
+                checkMethodIsKeptOrRemoved(
+                    dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
 
-              // No use of getter.
-              checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
-              checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
+                // No use of getter.
+                checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
+                checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
 
-              // No use of copy functions.
-              checkMethodIsRemoved(dataClass, COPY_METHOD);
-              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+                // No use of copy functions.
+                checkMethodIsRemoved(dataClass, COPY_METHOD);
+                checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              }
 
               ClassSubject classSubject = checkClassIsKept(inspector, mainClassName);
               MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
@@ -153,16 +169,15 @@
   public void test_dataclass_componentPartial() throws Exception {
     // TODO(b/179866251): Allow for CF code.
     assumeTrue(testParameters.isDexRuntime());
-    final String mainClassName = "dataclass.MainComponentPartialKt";
-    final MethodSignature testMethodSignature =
+    String mainClassName = "dataclass.MainComponentPartialKt";
+    MethodSignature testMethodSignature =
         new MethodSignature("testSomeDataClassComponentFunctions", "void", Collections.emptyList());
-    final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
     runTest(
             "dataclass",
             mainClassName,
             testBuilder ->
                 testBuilder
-                    .addKeepRules(extraRules)
+                    .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
@@ -195,45 +210,47 @@
 
   @Test
   public void test_dataclass_copyIsRemovedIfNotUsed() throws Exception {
-    final String mainClassName = "dataclass.MainComponentOnlyKt";
-    final MethodSignature testMethodSignature =
+    String mainClassName = "dataclass.MainComponentOnlyKt";
+    MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopy", "void", Collections.emptyList());
-    final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
     runTest(
             "dataclass",
             mainClassName,
             testBuilder ->
                 testBuilder
-                    .addKeepRules(extraRules)
+                    .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
-              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
-
-              checkMethodIsRemoved(dataClass, COPY_METHOD);
-              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              if (testParameters.isDexRuntime()
+                  && allowAccessModification
+                  && kotlinParameters.is(KotlinCompilerVersion.KOTLINC_1_3_72)) {
+                checkClassIsRemoved(inspector, TEST_DATA_CLASS.getClassName());
+              } else {
+                ClassSubject dataClass =
+                    checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
+                checkMethodIsRemoved(dataClass, COPY_METHOD);
+                checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              }
             });
   }
 
   @Test
   public void test_dataclass_copyDefaultIsRemovedIfNotUsed() throws Exception {
-    final String mainClassName = "dataclass.MainCopyKt";
-    final MethodSignature testMethodSignature =
+    String mainClassName = "dataclass.MainCopyKt";
+    MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopyWithDefault", "void", Collections.emptyList());
-    final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
     runTest(
             "dataclass",
             mainClassName,
             testBuilder ->
                 testBuilder
-                    .addKeepRules(extraRules)
+                    .addKeepRules(keepClassMethod(mainClassName, testMethodSignature))
                     .addOptionsModification(disableClassInliner))
         .inspect(
             inspector -> {
               ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
-
               checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
             });
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
index 996dc79..55a048e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
@@ -106,10 +106,8 @@
             if (rewrittenString.equals("L;") || rewrittenString.equals("(L;)V")) {
               return;
             }
-            System.out.println(clazzSubject.toString() + ": " + rewrittenString);
             addedNonInitStrings.increment();
           });
-      System.out.flush();
       assertEquals(originalHeader.getPackageName(), rewrittenHeader.getPackageName());
 
       String expected = KotlinMetadataWriter.kotlinMetadataToString("", originalMetadata);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInnerClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInnerClassTest.java
index c38a3d7..48007f1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInnerClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInnerClassTest.java
@@ -26,17 +26,19 @@
 @RunWith(Parameterized.class)
 public class MetadataRewriteInnerClassTest extends KotlinMetadataTestBase {
 
+  private static final String PKG_NESTED_REFLECT = PKG + ".nested_reflect";
   private static final String EXPECTED =
       StringUtils.lines(
-          "fun <init>(kotlin.Int):"
-              + " com.android.tools.r8.kotlin.metadata.nested_reflect.Outer.Nested",
-          "fun com.android.tools.r8.kotlin.metadata.nested_reflect.Outer.Inner.<init>(kotlin.Int):"
-              + " com.android.tools.r8.kotlin.metadata.nested_reflect.Outer.Inner");
+          "fun <init>(kotlin.Int): " + PKG_NESTED_REFLECT + ".Outer.Nested",
+          "fun "
+              + PKG_NESTED_REFLECT
+              + ".Outer.Inner.<init>(kotlin.Int): "
+              + PKG_NESTED_REFLECT
+              + ".Outer.Inner");
   private static final String EXPECTED_OUTER_RENAMED =
       StringUtils.lines(
-          "fun <init>(kotlin.Int): com.android.tools.r8.kotlin.metadata.nested_reflect.Nested",
-          "fun <init>(kotlin.Int): com.android.tools.r8.kotlin.metadata.nested_reflect.Inner");
-  private static final String PKG_NESTED_REFLECT = PKG + ".nested_reflect";
+          "fun <init>(kotlin.Int): " + PKG_NESTED_REFLECT + ".`Outer$Nested`",
+          "fun <init>(kotlin.Int): " + PKG_NESTED_REFLECT + ".`Outer$Inner`");
 
   private final TestParameters parameters;
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
index 605b16c..2c378c6 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
@@ -93,7 +93,10 @@
             .addProgramFiles(kotlincLibJar.getForConfiguration(kotlinc, targetVersion))
             .addClasspathFiles(getKotlinStdlibJar(kotlinc), getKotlinAnnotationJar(kotlinc))
             .addKeepAllClassesRule()
-            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            .addKeepAttributes(
+                ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS,
+                ProguardKeepAttributes.INNER_CLASSES,
+                ProguardKeepAttributes.ENCLOSING_METHOD)
             .compile()
             .inspect(this::inspect)
             .writeToZip();
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
index 80578a3..44092e7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
@@ -77,7 +77,10 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepKotlinMetadata()
-        .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+        .addKeepAttributes(
+            ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS,
+            ProguardKeepAttributes.INNER_CLASSES,
+            ProguardKeepAttributes.ENCLOSING_METHOD)
         .allowDiagnosticWarningMessages()
         .compile()
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
index ca441cd..9490133 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
@@ -14,7 +14,6 @@
 
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.codeinspector.AnnotationSubject;
@@ -59,12 +58,11 @@
             .addKeepMainRule(mainClassName)
             .addKeepKotlinMetadata()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
-            .allowDiagnosticMessages()
+            .allowDiagnosticWarningMessages()
             .setMinApi(parameters.getApiLevel())
             .allowUnusedDontWarnKotlinReflectJvmInternal(kotlinc.isNot(KOTLINC_1_3_72))
             .compile()
             .assertNoErrorMessages()
-            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .apply(KotlinMetadataTestBase::verifyExpectedWarningsFromKotlinReflectAndStdLib)
             .run(parameters.getRuntime(), mainClassName);
     CodeInspector inspector = result.inspector();
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index 8a27b3f..ca9fbd2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -11,12 +11,8 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.KotlinCompilerTool;
-import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
-import com.android.tools.r8.R8TestBuilder;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -64,18 +60,10 @@
               options.enableEnumValueOptimization = enableSwitchMapRemoval;
               options.enableEnumSwitchMapRemoval = enableSwitchMapRemoval;
             })
-        .applyIf(
-            kotlinParameters.getCompiler().is(KotlinCompilerVersion.KOTLINC_1_5_0_M2),
-            R8TestBuilder::allowDiagnosticMessages,
-            R8TestBuilder::allowDiagnosticWarningMessages)
         .setMinApi(parameters.getApiLevel())
         .noMinification()
+        .allowDiagnosticWarningMessages()
         .compile()
-        .assertNoErrorMessages()
-        .applyIf(
-            kotlinParameters.getCompiler().is(KotlinCompilerVersion.KOTLINC_1_5_0_M2),
-            TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation,
-            TestCompileResult::assertNoInfoMessages)
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .inspect(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassAllowShrinkingCompatibilityTest.java
new file mode 100644
index 0000000..5ee49f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassAllowShrinkingCompatibilityTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+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 KeepClassAllowShrinkingCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final Shrinker shrinker;
+
+  @Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(), ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public KeepClassAllowShrinkingCompatibilityTest(TestParameters parameters, Shrinker shrinker) {
+    this.parameters = parameters;
+    this.shrinker = shrinker;
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isPG()) {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    } else {
+      run(testForR8(parameters.getBackend()));
+    }
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    builder
+        .addProgramClasses(A.class, B.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules("-keep,allowshrinking" + " class " + A.class.getTypeName())
+        .addKeepRules("-keep,allowshrinking" + " class " + B.class.getTypeName())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aSubject = inspector.clazz(A.class);
+              assertThat(aSubject, isPresent());
+              ClassSubject bSubject = inspector.clazz(B.class);
+              assertThat(bSubject, isPresent());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class A {
+    public A(int i) {
+      // Non-default constructor to ensure no soft pinning of a members.
+      if (i == 42) {
+        throw new RuntimeException("Foo");
+      }
+    }
+  }
+
+  static class B {
+    public B(int i) {
+      // Non-default constructor to ensure no soft pinning of a members.
+      if (i == 42) {
+        throw new RuntimeException("Foo");
+      }
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A(args.length);
+      new B(args.length);
+    }
+  }
+}
diff --git a/third_party/internal-apps/youtube_15_33.tar.gz.sha1 b/third_party/internal-apps/youtube_15_33.tar.gz.sha1
index ad38c2b..9a04d6e 100644
--- a/third_party/internal-apps/youtube_15_33.tar.gz.sha1
+++ b/third_party/internal-apps/youtube_15_33.tar.gz.sha1
@@ -1 +1 @@
-a992d1cdf6b957a6e4b06361ea1b8edbaecc3088
\ No newline at end of file
+907babdaf04052eed13f22400175307131df1610
\ No newline at end of file
diff --git a/third_party/youtube/youtube.android_16.20.tar.gz.sha1 b/third_party/youtube/youtube.android_16.20.tar.gz.sha1
new file mode 100644
index 0000000..9e8e118
--- /dev/null
+++ b/third_party/youtube/youtube.android_16.20.tar.gz.sha1
@@ -0,0 +1 @@
+5e5ebc2236eae74b22886c51f46a88963f689efd
\ No newline at end of file
diff --git a/tools/create_jctf_tests.py b/tools/create_jctf_tests.py
index bc065d9..38e1f9c 100755
--- a/tools/create_jctf_tests.py
+++ b/tools/create_jctf_tests.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (c) 2017, 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.
@@ -9,7 +9,7 @@
 from os import makedirs
 from os.path import exists, join, dirname
 from shutil import rmtree
-from string import Template, upper
+from string import Template
 import os
 import re
 import sys
diff --git a/tools/download_from_x20.py b/tools/download_from_x20.py
index 5f64684..5796823 100755
--- a/tools/download_from_x20.py
+++ b/tools/download_from_x20.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # Copyright (c) 2016, 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.
@@ -20,7 +20,7 @@
   return optparse.OptionParser().parse_args()
 
 def download(src, dest):
-  print 'Downloading %s to %s' % (src, dest)
+  print('Downloading %s to %s' % (src, dest))
   shutil.copyfile(src, dest)
   utils.unpack_archive(dest)
 
@@ -29,21 +29,21 @@
   assert len(args) == 1
   sha1_file = args[0]
   dest = sha1_file[:-5]
-  print 'Ensuring %s' % dest
+  print('Ensuring %s' % dest)
   with open(sha1_file, 'r') as input_sha:
     sha1 = input_sha.readline()
   if os.path.exists(dest) and utils.get_sha1(dest) == sha1:
-    print 'sha1 matches, not downloading'
+    print('sha1 matches, not downloading')
     dest_dir = utils.extract_dir(dest)
     if os.path.exists(dest_dir):
-      print 'destination directory exists, no extraction'
+      print('destination directory exists, no extraction')
     else:
       utils.unpack_archive(dest)
     return
   src = os.path.join(GMSCORE_DEPS, sha1)
   if not os.path.exists(src):
-    print 'File (%s) does not exist on x20' % src
-    print 'Maybe pass -Pno_internal to your gradle invocation'
+    print('File (%s) does not exist on x20' % src)
+    print('Maybe pass -Pno_internal to your gradle invocation')
     return 42
   download(src, dest)
 
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index 9e729fe..6acce42 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -503,10 +503,12 @@
 
 def get_results_for_app(app, options, temp_dir):
   app_folder = app.folder if app.folder else app.name + "_" + app.revision
+  # Golem extraction will extract to the basename under the benchmarks dir.
+  app_location = os.path.basename(app_folder) if options.golem else app_folder
   opensource_basedir = (os.path.join('benchmarks', app.name) if options.golem
                         else utils.OPENSOURCE_DUMPS_DIR)
-  app_dir = (os.path.join(utils.INTERNAL_DUMPS_DIR, app_folder) if app.internal
-              else os.path.join(opensource_basedir, app_folder))
+  app_dir = (os.path.join(utils.INTERNAL_DUMPS_DIR, app_location) if app.internal
+              else os.path.join(opensource_basedir, app_location))
   if not os.path.exists(app_dir) and not options.golem:
     # Download the app from google storage.
     download_sha(app_dir + ".tar.gz.sha1", app.internal)
@@ -1006,6 +1008,7 @@
   jdk_gz = jdk.GetJdkHome() + '.tar.gz'
   download_sha(jdk_gz + '.sha1', False, quiet=True)
   jdk_sha256 = get_sha256(jdk_gz)
+  add_golem_resource(2, jdk_gz, 'openjdk', sha256=jdk_sha256)
   for app in options.apps:
     if app.folder and not app.internal:
       indentation = 2;
@@ -1022,13 +1025,16 @@
       print_indented('options.cpus = cpus;', indentation)
       print_indented('options.isScript = true;', indentation)
       print_indented('options.fromRevision = 9700;', indentation);
-      print_indented('options.mainFile = "tools/run_on_app_dump.py "', indentation)
-      print_indented('"--golem --shrinker r8 --app %s";' % app.name, indentation + 4)
+      print_indented('options.mainFile = "tools/run_on_app_dump.py "',
+                     indentation)
+      print_indented('"--golem --quiet --shrinker r8 --app %s";' % app.name,
+                     indentation + 4)
 
       app_gz = os.path.join(utils.OPENSOURCE_DUMPS_DIR, app.folder + '.tar.gz')
-      add_golem_resource(indentation, app_gz, 'app_resource')
-      add_golem_resource(indentation, jdk_gz, 'openjdk', sha256=jdk_sha256)
-
+      name = 'appResource'
+      add_golem_resource(indentation, app_gz, name)
+      print_indented('options.resources.add(appResource);', indentation)
+      print_indented('options.resources.add(openjdk);', indentation)
       print_indented('dumpsSuite.addBenchmark(name);', indentation)
       indentation = 2
       print_indented('}', indentation)
@@ -1044,10 +1050,13 @@
   print_indented('final %s = BenchmarkResource("",' % name, indentation)
   print_indented('type: BenchmarkResourceType.Storage,', indentation + 4)
   print_indented('uri: "gs://r8-deps/%s",' % sha, indentation + 4)
-  print_indented('hash:', indentation + 4)
-  print_indented('"%s",' % sha256, indentation + 8)
+  # Make dart formatter happy.
+  if indentation > 2:
+    print_indented('hash:', indentation + 4)
+    print_indented('"%s",' % sha256, indentation + 8)
+  else:
+    print_indented('hash: "%s",' % sha256, indentation + 4)
   print_indented('extract: "gz");', indentation + 4);
-  print_indented('options.resources.add(%s);' % name, indentation)
 
 def main(argv):
   (options, args) = parse_options(argv)
diff --git a/tools/tag_versions.py b/tools/tag_versions.py
new file mode 100755
index 0000000..ebf14ba
--- /dev/null
+++ b/tools/tag_versions.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import argparse
+import jdk
+import os.path
+import re
+import subprocess
+import sys
+import urllib.request
+
+import utils
+
+# Grep match string for 'Version X.Y.Z[-dev]'
+VERSION_EXP='^Version [[:digit:]]\+.[[:digit:]]\+.[[:digit:]]\+\(\|-dev\)$'
+
+# R8 is located in the 'builder' library.
+AGP_MAVEN="https://dl.google.com/android/maven2/com/android/tools/build/builder"
+
+def parse_options():
+  parser = argparse.ArgumentParser(description='Tag R8 Versions')
+  parser.add_argument(
+      '--branch',
+      help='The R8 branch to tag versions on, eg, origin/3.0')
+  parser.add_argument(
+      '--agp',
+      help='The AGP to compute the tag for, eg, 4.2.0-beta03')
+  parser.add_argument(
+      '--dry-run',
+      default=False,
+      action='store_true',
+      help='Print the state changing commands without running them.')
+  return parser.parse_args()
+
+def run(options, cmd):
+  print(' '.join(cmd))
+  if not options.dry_run:
+    subprocess.check_call(cmd)
+
+def main():
+  args = parse_options()
+  if args.branch:
+    tag_r8_branch(args.branch, args)
+  elif args.agp:
+    tag_agp_version(args.agp, args)
+  else:
+    print("Should use a top-level option, such as --branch or --agp.")
+    return 1
+  return 0
+
+def prepare_print_version(dist, temp):
+  wrapper_file = os.path.join(
+      utils.REPO_ROOT,
+      'src/main/java/com/android/tools/r8/utils/PrintR8Version.java')
+  cmd = [
+    jdk.GetJavacExecutable(),
+    wrapper_file,
+    '-d', temp,
+    '-cp', dist,
+  ]
+  utils.PrintCmd(cmd)
+  subprocess.check_output(cmd)
+  return temp
+
+def get_tag_info_on_origin(tag):
+  output = subprocess.check_output(
+      ['git', 'ls-remote', '--tags', 'origin', tag]).decode('utf-8')
+  if len(output.strip()) == 0:
+    return None
+  return output
+
+def tag_agp_version(agp, args):
+  tag = 'agp-%s' % agp
+  result = get_tag_info_on_origin(tag)
+  if result:
+    print('Tag %s is already present' % tag)
+    print(result)
+    subprocess.call(['git', 'show', '--oneline', '-s', tag])
+    return 0
+  with utils.TempDir() as temp:
+    url = "%s/%s/builder-%s.jar" % (AGP_MAVEN, agp, agp)
+    jar = os.path.join(temp, "agp.jar")
+    try:
+      urllib.request.urlretrieve(url, jar)
+    except urllib.error.HTTPError as e:
+      print('Could not find jar for agp %s' % agp)
+      print(e)
+      return 1
+    print_version_helper = prepare_print_version(utils.R8_JAR, temp)
+    output = subprocess.check_output([
+      jdk.GetJavaExecutable(),
+      '-cp', ':'.join([jar, print_version_helper]),
+      'com.android.tools.r8.utils.PrintR8Version'
+    ]).decode('utf-8')
+    version = output.split(' ')[0]
+    run(args, ['git', 'tag', '-f', tag, '-m', tag, '%s^{}' % version])
+    run(args, ['git', 'push', 'origin', tag])
+
+def tag_r8_branch(branch, args):
+  if not branch.startswith('origin/'):
+    print('Expected branch to start with origin/')
+    return 1
+  output = subprocess.check_output([
+    'git', 'log', '--pretty=format:%H\t%s', '--grep', VERSION_EXP, branch
+  ]).decode('utf-8')
+  for l in output.split('\n'):
+    (hash, subject) = l.split('\t')
+    m = re.search('Version (.+)', subject)
+    if not m:
+      print('Unable to find a version for line: %s' % l)
+      continue
+    version = m.group(1)
+    result = get_tag_info_on_origin(version)
+    if not result:
+      run(args, ['git', 'tag', '-a', version, '-m', version, hash])
+      run(args, ['git', 'push', 'origin', version])
+  if args.dry_run:
+    print('Dry run complete. None of the above have been executed.')
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/youtube_data.py b/tools/youtube_data.py
index d12cfb6..827041a 100644
--- a/tools/youtube_data.py
+++ b/tools/youtube_data.py
@@ -15,7 +15,10 @@
 V16_12_BASE = os.path.join(BASE, 'youtube.android_16.12')
 V16_12_PREFIX = os.path.join(V16_12_BASE, 'YouTubeRelease')
 
-LATEST_VERSION = '16.12'
+V16_20_BASE = os.path.join(BASE, 'youtube.android_16.20')
+V16_20_PREFIX = os.path.join(V16_20_BASE, 'YouTubeRelease')
+
+LATEST_VERSION = '16.20'
 
 VERSIONS = {
   '15.33': {
@@ -80,4 +83,35 @@
       'min-api' : ANDROID_L_API,
     }
   },
+  '16.20': {
+    'deploy' : {
+      'sanitize_libraries': False,
+      'inputs': ['%s_deploy.jar' % V16_20_PREFIX],
+      'libraries' : [
+          os.path.join(
+              V16_20_BASE,
+              'legacy_YouTubeRelease_combined_library_jars_filtered.jar')],
+      'pgconf': [
+          '%s_proguard.config' % V16_20_PREFIX,
+          '%s/proguardsettings/YouTubeRelease_proguard.config' % utils.THIRD_PARTY,
+          utils.IGNORE_WARNINGS_RULES],
+      'min-api' : ANDROID_L_API,
+      'android_java8_libs': {
+        'config': '%s/desugar_jdk_libs/full_desugar_jdk_libs.json' % V16_20_BASE,
+        # Intentionally not adding desugar_jdk_libs_configuration.jar since it
+        # is part of jdk_libs_to_desugar.jar in YouTube 16.20.
+        'program': ['%s/desugar_jdk_libs/jdk_libs_to_desugar.jar' % V16_20_BASE],
+        'library': '%s/android_jar/lib-v30/android.jar' % utils.THIRD_PARTY,
+        'pgconf': [
+          '%s/desugar_jdk_libs/base.pgcfg' % V16_20_BASE,
+          '%s/desugar_jdk_libs/minify_desugar_jdk_libs.pgcfg' % V16_20_BASE
+        ]
+      }
+    },
+    'proguarded' : {
+      'inputs': ['%s_proguard.jar' % V16_20_PREFIX],
+      'pgmap': '%s_proguard.map' % V16_20_PREFIX,
+      'min-api' : ANDROID_L_API,
+    }
+  },
 }