Merge commit '401798770b3c9f8529a1d582c969dc2afb3ee756' into dev-release
diff --git a/infra/config/global/luci-milo.cfg b/infra/config/global/luci-milo.cfg
index 1c2c87f..7400340 100644
--- a/infra/config/global/luci-milo.cfg
+++ b/infra/config/global/luci-milo.cfg
@@ -26,11 +26,6 @@
     short_name: "jdk9"
   }
   builders {
-    name: "buildbucket/luci.r8.ci/linux-jdk8_9"
-    category: "R8"
-    short_name: "jdk8_9"
-  }
-  builders {
     name: "buildbucket/luci.r8.ci/linux-android-4.0.4"
     category: "R8"
     short_name: "4.0.4"
diff --git a/infra/config/global/luci-scheduler.cfg b/infra/config/global/luci-scheduler.cfg
index a18a124..af1ff68 100644
--- a/infra/config/global/luci-scheduler.cfg
+++ b/infra/config/global/luci-scheduler.cfg
@@ -84,12 +84,23 @@
   triggers: "linux-android-10.0.0_release"
   triggers: "linux-internal_release"
   triggers: "linux-jctf_release"
-  triggers: "linux-run-on-app-dump_release"
   triggers: "linux_release"
   triggers: "r8cf-linux-jctf_release"
   triggers: "windows_release"
 }
 
+trigger {
+  id: "app-dump-gitiles-trigger"
+  acl_sets: "default"
+  gitiles: {
+    repo: "https://r8.googlesource.com/r8"
+    # Only trigger apps from 3.0 (this works until we reach version 10)
+    refs: "regexp:refs/heads/[3-9]+\\.[0-9]+(\\.[0-9]+)?"
+    path_regexps: "src/main/java/com/android/tools/r8/Version.java"
+  }
+  triggers: "linux-run-on-app-dump_release"
+}
+
 
 job {
   id: "archive"
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 8e56a8e..8a88d3d 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -290,9 +290,11 @@
           // desugared library only on cf inputs. We cannot easily rewrite part of the program
           // without iterating again the IR. We fall-back to writing one app with rewriting and
           // merging it with the other app in rewriteNonDexInputs.
+          timing.begin("Rewrite non-dex inputs");
           DexApplication app =
               rewriteNonDexInputs(
                   appView, inputApp, options, executor, timing, appView.appInfo().app());
+          timing.end();
           appView.setAppInfo(
               new AppInfo(
                   appView.appInfo().getSyntheticItems().commit(app),
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 8b5d011..0230729 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -87,6 +87,7 @@
           getDistribution(app, featureClassMapping, mapper);
       for (Entry<String, LazyLoadedDexApplication.Builder> entry : applications.entrySet()) {
         String feature = entry.getKey();
+        timing.begin("Feature " + feature);
         DexApplication featureApp = entry.getValue().build();
         assert !options.hasMethodsFilter();
 
@@ -121,6 +122,7 @@
         } finally {
           consumer.finished(options.reporter);
         }
+        timing.end();
       }
     } catch (ExecutionException e) {
       throw unwrapExecutionException(e);
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index a4ce917..da40e79 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -146,7 +146,7 @@
     List<DexEncodedMethod> directMethods = new ArrayList<>();
     List<DexEncodedMethod> virtualMethods = new ArrayList<>();
     for (DexEncodedMethod method : methods) {
-      assert method.holder() == clazz.type;
+      assert method.getHolderType() == clazz.type;
       CfCode code = null;
       if (!method.accessFlags.isAbstract() /*&& !method.accessFlags.isNative()*/) {
         code = buildEmptyThrowingCfCode(method.method);
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
index 2f53841..3d039c9 100644
--- a/src/main/java/com/android/tools/r8/PrintUses.java
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -201,7 +201,7 @@
     private void addField(DexField field) {
       addType(field.type);
       DexEncodedField baseField = appInfo.resolveField(field).getResolvedField();
-      if (baseField != null && baseField.holder() != field.holder) {
+      if (baseField != null && baseField.getHolderType() != field.holder) {
         field = baseField.field;
       }
       addType(field.holder);
@@ -215,7 +215,7 @@
           noObfuscationTypes.add(field.holder);
         }
         if (baseField.accessFlags.isVisibilityDependingOnPackage()) {
-          keepPackageNames.add(baseField.holder().getPackageName());
+          keepPackageNames.add(baseField.getHolderType().getPackageName());
         }
         typeFields.add(field);
       }
@@ -236,7 +236,7 @@
           noObfuscationTypes.add(method.holder);
         }
         if (definition.accessFlags.isVisibilityDependingOnPackage()) {
-          keepPackageNames.add(definition.holder().getPackageName());
+          keepPackageNames.add(definition.getHolderType().getPackageName());
         }
         typeMethods.add(method);
       }
@@ -496,7 +496,7 @@
       if (encodedMethod.accessFlags.isStatic()) {
         append("<clinit>");
       } else {
-        String holderName = encodedMethod.holder().toSourceString();
+        String holderName = encodedMethod.getHolderType().toSourceString();
         String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
         append(constructorName);
       }
diff --git a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
index 93fc650..28d7592 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -302,7 +302,7 @@
 
   private void updateHints(LiveIntervals intervals) {
     for (Phi phi : intervals.getValue().uniquePhiUsers()) {
-      if (!phi.isValueOnStack()) {
+      if (!phi.isValueOnStack() && phi.getLiveIntervals().getHint() == null) {
         phi.getLiveIntervals().setHint(intervals, unhandled);
         for (Value value : phi.getOperands()) {
           value.getLiveIntervals().setHint(intervals, unhandled);
diff --git a/src/main/java/com/android/tools/r8/cf/CfVersion.java b/src/main/java/com/android/tools/r8/cf/CfVersion.java
index d90696c..f606b93 100644
--- a/src/main/java/com/android/tools/r8/cf/CfVersion.java
+++ b/src/main/java/com/android/tools/r8/cf/CfVersion.java
@@ -3,14 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf;
 
-import com.android.tools.r8.utils.structural.DefaultCompareToVisitor;
 import com.android.tools.r8.utils.structural.Equatable;
 import com.android.tools.r8.utils.structural.HashCodeVisitor;
-import com.android.tools.r8.utils.structural.Ordered;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import org.objectweb.asm.Opcodes;
 
-public final class CfVersion implements Ordered<CfVersion> {
+public final class CfVersion implements StructuralItem<CfVersion> {
 
   public static final CfVersion V1_1 = new CfVersion(Opcodes.V1_1);
   public static final CfVersion V1_2 = new CfVersion(Opcodes.V1_2);
@@ -47,23 +47,28 @@
     return version;
   }
 
-  private static void accept(StructuralSpecification<CfVersion, ?> spec) {
+  private static void specify(StructuralSpecification<CfVersion, ?> spec) {
     spec.withInt(CfVersion::major).withInt(CfVersion::minor);
   }
 
   @Override
+  public CfVersion self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<CfVersion> getStructuralAccept() {
+    return CfVersion::specify;
+  }
+
+  @Override
   public boolean equals(Object o) {
     return Equatable.equalsImpl(this, o);
   }
 
   @Override
   public int hashCode() {
-    return HashCodeVisitor.run(this, CfVersion::accept);
-  }
-
-  @Override
-  public int compareTo(CfVersion other) {
-    return DefaultCompareToVisitor.run(this, other, CfVersion::accept);
+    return HashCodeVisitor.run(this, CfVersion::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
index 4574386..ec4b22f 100644
--- a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
@@ -238,8 +238,8 @@
         if (argumentIndex < 0) {
           argumentType =
               code.method().isInstanceInitializer()
-                  ? new ThisInstanceInfo(instruction.asArgument(), code.method().holder())
-                  : createInitializedType(code.method().holder());
+                  ? new ThisInstanceInfo(instruction.asArgument(), code.method().getHolderType())
+                  : createInitializedType(code.method().getHolderType());
         } else {
           argumentType =
               createInitializedType(code.method().method.proto.parameters.values[argumentIndex]);
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
index c05c20d..7cfcf5b 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -51,8 +52,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   public Opcode getOpcode() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
index 70b41e1..44699bf 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
index ae086f3..a8d3546 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -41,8 +42,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   public MemberType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
index eb43f6b..fa27c39 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   private int getStoreType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
index a391dbb..b4b33b1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(((CfCheckCast) other).type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(((CfCheckCast) other).type, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
index 23b0c8b..d1d4bb9 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -48,8 +49,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   public Bias getBias() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
index ced7984..7931528 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Type;
@@ -39,8 +40,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(((CfConstClass) other).type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(((CfConstClass) other).type, visitor);
   }
 
   public DexType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
index 9c3b478..17c2abe 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return handle.compareTo(((CfConstMethodHandle) other).handle);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    handle.acceptCompareTo(((CfConstMethodHandle) other).handle, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
index c285c80..85f125e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Type;
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(((CfConstMethodType) other).type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(((CfConstMethodType) other).type, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
index 1835f18..7575c49 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
index e48643e..23f3a2d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
@@ -20,7 +20,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -40,10 +40,12 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Comparator.comparing(CfConstNumber::getRawValue)
-        .thenComparing(CfConstNumber::getType)
-        .compare(this, (CfConstNumber) other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(
+        this,
+        (CfConstNumber) other,
+        spec -> spec.withLong(CfConstNumber::getRawValue).withItem(CfConstNumber::getType));
   }
 
   public ValueType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
index 7ac89da..827e77e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 
 public class CfConstString extends CfInstruction {
@@ -35,8 +36,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return string.compareTo(other.asConstString().string);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    string.acceptCompareTo(other.asConstString().string, visitor);
   }
 
   public DexString getString() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
index b42194f..e880a23 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 
@@ -42,8 +43,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return item.referenceCompareTo(((CfDexItemBasedConstString) other).item);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visitDexReference(item, ((CfDexItemBasedConstString) other).item);
   }
 
   public DexReference getItem() {
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 0915f0d..978a43d 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
@@ -23,7 +23,8 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -34,6 +35,10 @@
   private final DexField field;
   private final DexField declaringField;
 
+  private static void specify(StructuralSpecification<CfFieldInstruction, ?> spec) {
+    spec.withInt(f -> f.opcode).withItem(f -> f.field).withItem(f -> f.declaringField);
+  }
+
   public CfFieldInstruction(int opcode, DexField field, DexField declaringField) {
     this.opcode = opcode;
     this.field = field;
@@ -55,10 +60,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Comparator.comparing(CfFieldInstruction::getField)
-        .thenComparing(field -> field.declaringField)
-        .compare(this, (CfFieldInstruction) other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(this, other.asFieldInstruction(), CfFieldInstruction::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index cdcb3ec..d89ef90 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.util.ArrayDeque;
@@ -152,10 +153,11 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     // The frame should be determined by the code so it should for equal iff the code is equal.
     // Thus we just require the frame to be in place.
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   private static class InitializedType extends FrameType {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
index 0867a89..7b3ccf8 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -35,8 +36,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return helper.compareLabels(target, ((CfGoto) other).target);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    helper.compareLabels(target, ((CfGoto) other).target, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIf.java b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
index ddca593..6e00da7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,11 +44,12 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     CfIf otherIf = (CfIf) other;
     assert kind == otherIf.kind;
     assert type == otherIf.type;
-    return helper.compareLabels(target, otherIf.target);
+    helper.compareLabels(target, otherIf.target, visitor);
   }
 
   public ValueType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
index efedff8..35f9af3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,11 +44,12 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     CfIfCmp otherIf = (CfIfCmp) other;
     assert kind == otherIf.kind;
     assert type == otherIf.type;
-    return helper.compareLabels(target, otherIf.target);
+    helper.compareLabels(target, otherIf.target, visitor);
   }
 
   public Type getKind() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
index 9e59ba6..04a7599 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
@@ -19,7 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -39,10 +39,12 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Comparator.comparingInt(CfIinc::getLocalIndex)
-        .thenComparing(CfIinc::getIncrement)
-        .compare(this, (CfIinc) other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(
+        this,
+        (CfIinc) other,
+        spec -> spec.withInt(CfIinc::getLocalIndex).withInt(CfIinc::getIncrement));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
index 53437ec..4d311fe 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 
@@ -48,8 +49,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return clazz.compareTo(((CfInitClass) other).clazz);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    clazz.acceptCompareTo(((CfInitClass) other).clazz, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
index 1bbb32a..85d9b59 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -42,8 +43,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(other.asInstanceOf().type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(other.asInstanceOf().type, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
index 3c36d33..d4986b3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 
@@ -60,11 +61,17 @@
    * <p>If an instruction is uniquely determined by the "compare id" then the override should simply
    * call '{@code CfCompareHelper::compareIdUniquelyDeterminesEquality}'.
    */
-  public abstract int internalCompareTo(CfInstruction other, CfCompareHelper helper);
+  public abstract void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper);
 
-  public final int compareTo(CfInstruction o, CfCompareHelper helper) {
+  public final void acceptCompareTo(
+      CfInstruction o, CompareToVisitor visitor, CfCompareHelper helper) {
     int diff = getCompareToId() - o.getCompareToId();
-    return diff != 0 ? diff : internalCompareTo(o, helper);
+    if (diff == 0) {
+      internalAcceptCompareTo(o, visitor, helper);
+    } else {
+      visitor.visitInt(getCompareToId(), o.getCompareToId());
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 3e07e17..4d1af96 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -33,6 +33,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.Arrays;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
@@ -59,10 +60,13 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     CfInvoke otherInvoke = other.asInvoke();
-    int itfDiff = Boolean.compare(itf, otherInvoke.itf);
-    return itfDiff != 0 ? itfDiff : method.compareTo(otherInvoke.method);
+    visitor.visit(
+        this,
+        otherInvoke,
+        spec -> spec.withBool(CfInvoke::isInterface).withItem(CfInvoke::getMethod));
   }
 
   public DexMethod getMethod() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index 92384c8..fc1f3e5 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ListIterator;
@@ -48,8 +49,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return callSite.compareTo(((CfInvokeDynamic) other).callSite);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    callSite.acceptCompareTo(((CfInvokeDynamic) other).callSite, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
index ed6c16f..d8cd219 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 
 public class CfJsrRet extends CfInstruction {
@@ -41,7 +42,8 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     throw error();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
index 2bfdd93..dc6d863 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
 
@@ -38,8 +39,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return helper.compareLabels(this, other.asLabel());
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    helper.compareLabels(this, other.asLabel(), visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
index 3160d6e..4e48880 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -40,8 +41,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Integer.compare(var, other.asLoad().var);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visitInt(var, other.asLoad().var);
   }
 
   private int getLoadType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
index d7f2c7d..d6264f7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -53,8 +54,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   public NumericType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
index 8225673..520c168 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -42,8 +43,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index a504f8c..ff9ea71 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -21,7 +21,7 @@
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.InternalOptions;
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -50,10 +50,12 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Comparator.comparingInt(CfMultiANewArray::getDimensions)
-        .thenComparing(CfMultiANewArray::getType)
-        .compare(this, ((CfMultiANewArray) other));
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(
+        this,
+        (CfMultiANewArray) other,
+        spec -> spec.withInt(CfMultiANewArray::getDimensions).withItem(CfMultiANewArray::getType));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
index a7b8c32..dce1028 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNew.java b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
index 41f468c..87a407d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNew.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -43,8 +44,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(((CfNew) other).type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(((CfNew) other).type, visitor);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
index 414bd58..0dbf78e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ListIterator;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -46,8 +47,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.compareTo(((CfNewArray) other).type);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    type.acceptCompareTo(((CfNewArray) other).type, visitor);
   }
 
   private int getPrimitiveTypeCode() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNop.java b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
index 87123a4..0a69673b 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -29,8 +30,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
index a7e60e4..35c78a7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -45,8 +46,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   public NumericType getFromType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
index 233a3ef..df8612e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 
 public class CfPosition extends CfInstruction {
@@ -37,10 +38,14 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    CfPosition otherPosition = (CfPosition) other;
-    int lineDiff = position.line - otherPosition.position.line;
-    return lineDiff != 0 ? lineDiff : helper.compareLabels(label, otherPosition.label);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(
+        this,
+        (CfPosition) other,
+        spec ->
+            spec.withInt(p -> p.position.line)
+                .withCustomItem(p -> p.label, helper.labelAcceptor()));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
index 8305860..b09fbea 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -42,8 +43,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   private int getOpcode() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
index 687d903..c52efe3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -34,8 +35,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
index c28cb32..c6c39be 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -87,8 +88,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStore.java b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
index debb573..e424474 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -41,8 +42,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Integer.compare(var, other.asStore().var);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visitInt(var, other.asStore().var);
   }
 
   private int getStoreType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
index ddfd927..86abb2e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
-import static com.android.tools.r8.utils.ComparatorUtils.listComparator;
-
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCompareHelper;
@@ -21,10 +19,9 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
-import java.util.Comparator;
 import java.util.List;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
@@ -54,12 +51,17 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
     assert kind == ((CfSwitch) other).kind;
-    return Comparator.comparing(CfSwitch::getDefaultTarget, helper::compareLabels)
-        .thenComparing(insn -> insn.keys, ComparatorUtils::compareIntArray)
-        .thenComparing(CfSwitch::getSwitchTargets, listComparator(helper::compareLabels))
-        .compare(this, (CfSwitch) other);
+
+    visitor.visit(
+        this,
+        (CfSwitch) other,
+        spec ->
+            spec.withCustomItem(CfSwitch::getDefaultTarget, helper.labelAcceptor())
+                .withIntArray(i -> i.keys)
+                .withCustomItemCollection(CfSwitch::getSwitchTargets, helper.labelAcceptor()));
   }
 
   public Kind getKind() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
index b165711..7243175 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -35,8 +36,9 @@
   }
 
   @Override
-  public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
+  public void internalAcceptCompareTo(
+      CfInstruction other, CompareToVisitor visitor, CfCompareHelper helper) {
+    CfCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
index a93f320..6cc3b13 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
@@ -10,9 +10,8 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.conversion.CfBuilder;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.List;
 
 public class CfTryCatch {
@@ -49,12 +48,15 @@
     return new CfTryCatch(start, end, guards, targets);
   }
 
-  public int compareTo(CfTryCatch other, CfCompareHelper helper) {
-    return Comparator.comparing((CfTryCatch c) -> c.start, helper::compareLabels)
-        .thenComparing(c -> c.end, helper::compareLabels)
-        .thenComparing(c -> c.guards, ComparatorUtils.listComparator())
-        .thenComparing(c -> c.targets, ComparatorUtils.listComparator(helper::compareLabels))
-        .compare(this, other);
+  public void acceptCompareTo(CfTryCatch other, CompareToVisitor visitor, CfCompareHelper helper) {
+    visitor.visit(
+        this,
+        other,
+        spec ->
+            spec.withCustomItem(c -> c.start, helper.labelAcceptor())
+                .withCustomItem(c -> c.end, helper.labelAcceptor())
+                .withItemCollection(c -> c.guards)
+                .withCustomItemCollection(c -> c.targets, helper.labelAcceptor()));
   }
 
   public void internalRegisterUse(UseRegistry registry, DexClassAndMethod context) {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 2ad4409..ab72b04 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -54,6 +54,8 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.Timing.TimingMerger;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ObjectArrays;
 import it.unimi.dsi.fastutil.objects.Reference2LongMap;
@@ -229,7 +231,8 @@
   }
 
   public void write(ExecutorService executorService) throws IOException, ExecutionException {
-    appView.appInfo().app().timing.begin("DexApplication.write");
+    Timing timing = appView.appInfo().app().timing;
+    timing.begin("DexApplication.write");
     ProguardMapId proguardMapId = null;
     if (proguardMapSupplier != null && options.proguardMapConsumer != null) {
       proguardMapId = proguardMapSupplier.writeProguardMap();
@@ -248,19 +251,27 @@
       }
     }
     try {
+      timing.begin("Insert Attribute Annotations");
       // TODO(b/151313715): Move this to the writer threads.
       insertAttributeAnnotations();
+      timing.end();
 
       // Each DexCallSite must have its instruction offset set for sorting.
       if (options.isGeneratingDex()) {
+        timing.begin("Set call-site contexts");
         setCallSiteContexts(executorService);
+        timing.end();
       }
 
       // Generate the dex file contents.
       List<Future<Boolean>> dexDataFutures = new ArrayList<>();
+      timing.begin("Distribute");
       List<VirtualFile> virtualFiles = distribute(executorService);
+      timing.end();
       if (options.encodeChecksums) {
+        timing.begin("Encode checksums");
         encodeChecksums(virtualFiles);
+        timing.end();
       }
       assert markers == null
           || markers.isEmpty()
@@ -270,69 +281,25 @@
           true);
 
       // TODO(b/151313617): Sorting annotations mutates elements so run single threaded on main.
+      timing.begin("Sort Annotations");
       SortAnnotations sortAnnotations = new SortAnnotations(namingLens);
       appView.appInfo().classes().forEach((clazz) -> clazz.addDependencies(sortAnnotations));
+      timing.end();
 
-      for (VirtualFile virtualFile : virtualFiles) {
-        if (virtualFile.isEmpty()) {
-          continue;
-        }
-        dexDataFutures.add(
-            executorService.submit(
-                () -> {
-                  ProgramConsumer consumer;
-                  ByteBufferProvider byteBufferProvider;
-                  if (programConsumer != null) {
-                    consumer = programConsumer;
-                    byteBufferProvider = programConsumer;
-                  } else if (virtualFile.getPrimaryClassDescriptor() != null) {
-                    consumer = options.getDexFilePerClassFileConsumer();
-                    byteBufferProvider = options.getDexFilePerClassFileConsumer();
-                  } else {
-                    if (virtualFile.getFeatureSplit() != null) {
-                      ProgramConsumer featureConsumer =
-                          virtualFile.getFeatureSplit().getProgramConsumer();
-                      assert featureConsumer instanceof DexIndexedConsumer;
-                      consumer = featureConsumer;
-                      byteBufferProvider = (DexIndexedConsumer) featureConsumer;
-                    } else {
-                      consumer = options.getDexIndexedConsumer();
-                      byteBufferProvider = options.getDexIndexedConsumer();
-                    }
-                  }
-                  ObjectToOffsetMapping objectMapping =
-                      virtualFile.computeMapping(appView, graphLens, namingLens, initClassLens);
-                  MethodToCodeObjectMapping codeMapping =
-                      rewriteCodeWithJumboStrings(
-                          objectMapping, virtualFile.classes(), appView.appInfo().app());
-                  ByteBufferResult result =
-                      writeDexFile(objectMapping, codeMapping, byteBufferProvider);
-                  ByteDataView data =
-                      new ByteDataView(
-                          result.buffer.array(), result.buffer.arrayOffset(), result.length);
-                  if (consumer instanceof DexFilePerClassFileConsumer) {
-                    ((DexFilePerClassFileConsumer) consumer)
-                        .accept(
-                            virtualFile.getPrimaryClassDescriptor(),
-                            data,
-                            virtualFile.getClassDescriptors(),
-                            options.reporter);
-                  } else {
-                    ((DexIndexedConsumer) consumer)
-                        .accept(
-                            virtualFile.getId(),
-                            data,
-                            virtualFile.getClassDescriptors(),
-                            options.reporter);
-                  }
-                  // Release use of the backing buffer now that accept has returned.
-                  data.invalidate();
-                  byteBufferProvider.releaseByteBuffer(result.buffer.asByteBuffer());
-                  return true;
-                }));
-      }
-      // Wait for all files to be processed before moving on.
-      ThreadUtils.awaitFutures(dexDataFutures);
+      TimingMerger merger =
+          timing.beginMerger("Write files", ThreadUtils.getNumberOfThreads(executorService));
+      Collection<Timing> timings =
+          ThreadUtils.processItemsWithResults(
+              virtualFiles,
+              virtualFile -> {
+                Timing fileTiming = Timing.create("VirtualFile " + virtualFile.getId(), options);
+                writeVirtualFile(virtualFile, fileTiming);
+                fileTiming.end();
+                return fileTiming;
+              },
+              executorService);
+      merger.add(timings);
+      merger.end();
       // A consumer can manage the generated keep rules.
       if (options.desugaredLibraryKeepRuleConsumer != null && !desugaredLibraryCodeToKeep.isNop()) {
         assert !options.isDesugaredLibraryCompilation();
@@ -343,10 +310,64 @@
       // Supply info to all additional resource consumers.
       supplyAdditionalConsumers(appView.appInfo().app(), appView, graphLens, namingLens, options);
     } finally {
-      appView.appInfo().app().timing.end();
+      timing.end();
     }
   }
 
+  private void writeVirtualFile(VirtualFile virtualFile, Timing timing) {
+    if (virtualFile.isEmpty()) {
+      return;
+    }
+    ProgramConsumer consumer;
+    ByteBufferProvider byteBufferProvider;
+    if (programConsumer != null) {
+      consumer = programConsumer;
+      byteBufferProvider = programConsumer;
+    } else if (virtualFile.getPrimaryClassDescriptor() != null) {
+      consumer = options.getDexFilePerClassFileConsumer();
+      byteBufferProvider = options.getDexFilePerClassFileConsumer();
+    } else {
+      if (virtualFile.getFeatureSplit() != null) {
+        ProgramConsumer featureConsumer = virtualFile.getFeatureSplit().getProgramConsumer();
+        assert featureConsumer instanceof DexIndexedConsumer;
+        consumer = featureConsumer;
+        byteBufferProvider = (DexIndexedConsumer) featureConsumer;
+      } else {
+        consumer = options.getDexIndexedConsumer();
+        byteBufferProvider = options.getDexIndexedConsumer();
+      }
+    }
+    timing.begin("Compute object offset mapping");
+    ObjectToOffsetMapping objectMapping =
+        virtualFile.computeMapping(appView, graphLens, namingLens, initClassLens, timing);
+    timing.end();
+    timing.begin("Rewrite jumbo strings");
+    MethodToCodeObjectMapping codeMapping =
+        rewriteCodeWithJumboStrings(objectMapping, virtualFile.classes(), appView.appInfo().app());
+    timing.end();
+    timing.begin("Write bytes");
+    ByteBufferResult result = writeDexFile(objectMapping, codeMapping, byteBufferProvider);
+    ByteDataView data =
+        new ByteDataView(result.buffer.array(), result.buffer.arrayOffset(), result.length);
+    timing.end();
+    timing.begin("Pass bytes to consumer");
+    if (consumer instanceof DexFilePerClassFileConsumer) {
+      ((DexFilePerClassFileConsumer) consumer)
+          .accept(
+              virtualFile.getPrimaryClassDescriptor(),
+              data,
+              virtualFile.getClassDescriptors(),
+              options.reporter);
+    } else {
+      ((DexIndexedConsumer) consumer)
+          .accept(virtualFile.getId(), data, virtualFile.getClassDescriptors(), options.reporter);
+    }
+    timing.end();
+    // Release use of the backing buffer now that accept has returned.
+    data.invalidate();
+    byteBufferProvider.releaseByteBuffer(result.buffer.asByteBuffer());
+  }
+
   public static void supplyAdditionalConsumers(
       DexApplication application,
       AppView<?> appView,
diff --git a/src/main/java/com/android/tools/r8/dex/DexReader.java b/src/main/java/com/android/tools/r8/dex/DexReader.java
index 7e3f1dc..18c0beb 100644
--- a/src/main/java/com/android/tools/r8/dex/DexReader.java
+++ b/src/main/java/com/android/tools/r8/dex/DexReader.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.DexVersion;
+import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteOrder;
@@ -47,8 +48,22 @@
     }
     int index = 0;
     for (byte prefixByte : DEX_FILE_MAGIC_PREFIX) {
-      if (buffer.get(index++) != prefixByte) {
-        throw new CompilationError("Dex file has invalid header", origin);
+      byte actualByte = buffer.get(index++);
+      if (actualByte != prefixByte) {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(
+            "Dex file has invalid header, expected "
+                + prefixByte
+                + " got "
+                + actualByte
+                + ". Next bytes are ");
+        for (int i = 0; i < 10; i++) {
+          if (buffer.hasRemaining()) {
+            stringBuilder.append(StringUtils.hexString(buffer.get(), 2));
+            stringBuilder.append(",");
+          }
+        }
+        throw new CompilationError(stringBuilder.toString(), origin);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 108df1d..bce34ff 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Sets;
@@ -209,7 +210,11 @@
   }
 
   public ObjectToOffsetMapping computeMapping(
-      AppView<?> appView, GraphLens graphLens, NamingLens namingLens, InitClassLens initClassLens) {
+      AppView<?> appView,
+      GraphLens graphLens,
+      NamingLens namingLens,
+      InitClassLens initClassLens,
+      Timing timing) {
     assert transaction.isEmpty();
     return new ObjectToOffsetMapping(
         appView,
@@ -223,7 +228,8 @@
         indexedItems.fields,
         indexedItems.strings,
         indexedItems.callSites,
-        indexedItems.methodHandles);
+        indexedItems.methodHandles,
+        timing);
   }
 
   void addClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index de254fc..910153a 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -4,12 +4,15 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.function.BooleanSupplier;
 
 /** Access flags common to classes, methods and fields. */
-public abstract class AccessFlags<T extends AccessFlags<T>> {
+public abstract class AccessFlags<T extends AccessFlags<T>> implements StructuralItem<T> {
 
   protected static final int BASE_FLAGS
       = Constants.ACC_PUBLIC
@@ -53,8 +56,18 @@
     this.modifiedFlags = modifiedFlags;
   }
 
+  protected static <T extends AccessFlags<T>> void specify(StructuralSpecification<T, ?> spec) {
+    spec.withInt(a -> a.originalFlags).withInt(a -> a.modifiedFlags);
+  }
+
+  @Override
+  public StructuralAccept<T> getStructuralAccept() {
+    return AccessFlags::specify;
+  }
+
   public abstract T copy();
 
+  @Override
   public abstract T self();
 
   public int materialize() {
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 70db016..1e7ec9c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -94,6 +94,10 @@
 
   private Map<DexClass, DexValueString> sourceDebugExtensions = new IdentityHashMap<>();
 
+  // When input has been (partially) desugared these are the classes which has been library
+  // desugared. This information is populated in the IR converter.
+  private Set<DexProgramClass> alreadyLibraryDesugared = null;
+
   private AppView(
       T appInfo,
       WholeProgramOptimizations wholeProgramOptimizations,
@@ -663,4 +667,17 @@
           }
         });
   }
+
+  public void setAlreadyLibraryDesugared(Set<DexProgramClass> alreadyLibraryDesugared) {
+    assert this.alreadyLibraryDesugared == null;
+    this.alreadyLibraryDesugared = alreadyLibraryDesugared;
+  }
+
+  public boolean isAlreadyLibraryDesugared(DexProgramClass clazz) {
+    if (!options().desugarSpecificOptions().allowAllDesugaredInput) {
+      return false;
+    }
+    assert alreadyLibraryDesugared != null;
+    return alreadyLibraryDesugared.contains(clazz);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index c17ff09..6d3cedc 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -41,19 +41,20 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
 import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.ListIterator;
@@ -63,7 +64,7 @@
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
 
-public class CfCode extends Code implements Comparable<CfCode> {
+public class CfCode extends Code implements StructuralItem<CfCode> {
 
   public enum StackMapStatus {
     NOT_VERIFIED,
@@ -111,12 +112,16 @@
       return end;
     }
 
-    public int compareTo(LocalVariableInfo other, CfCompareHelper helper) {
-      return Comparator.comparingInt(LocalVariableInfo::getIndex)
-          .thenComparing(LocalVariableInfo::getStart, helper::compareLabels)
-          .thenComparing(LocalVariableInfo::getEnd, helper::compareLabels)
-          .thenComparing(LocalVariableInfo::getLocal)
-          .compare(this, other);
+    public void acceptCompareTo(
+        LocalVariableInfo other, CompareToVisitor visitor, CfCompareHelper helper) {
+      visitor.visit(
+          this,
+          other,
+          spec ->
+              spec.withInt(LocalVariableInfo::getIndex)
+                  .withCustomItem(LocalVariableInfo::getStart, helper.labelAcceptor())
+                  .withCustomItem(LocalVariableInfo::getEnd, helper.labelAcceptor())
+                  .withItem(LocalVariableInfo::getLocal));
     }
 
     @Override
@@ -153,6 +158,16 @@
     this.localVariables = localVariables;
   }
 
+  @Override
+  public CfCode self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<CfCode> getStructuralAccept() {
+    throw new Unreachable();
+  }
+
   public DexType getOriginalHolder() {
     return originalHolder;
   }
@@ -229,38 +244,25 @@
   }
 
   @Override
-  public int compareTo(CfCode o) {
-    // Fast path by checking sizes.
-    int sizeDiff =
-        Comparator.comparingInt((CfCode c) -> c.instructions.size())
-            .thenComparingInt(c -> c.tryCatchRanges.size())
-            .thenComparingInt(c -> localVariables.size())
-            .compare(this, o);
-    if (sizeDiff != 0) {
-      return sizeDiff;
-    }
-    // In the slow case, compute label maps and compare collections in full.
-    Reference2IntMap<CfLabel> labels1 = getLabelOrdering(instructions);
-    Reference2IntMap<CfLabel> labels2 = getLabelOrdering(o.instructions);
-    int labelDiff = labels1.size() - labels2.size();
-    if (labelDiff != 0) {
-      return labelDiff;
-    }
-    CfCompareHelper helper = new CfCompareHelper(labels1, labels2);
-    return Comparator.comparing((CfCode c) -> c.instructions, helper.instructionComparator())
-        .thenComparing(c -> c.tryCatchRanges, helper.tryCatchRangesComparator())
-        .thenComparing(c -> c.localVariables, helper.localVariablesComparator())
-        .compare(this, o);
+  public void acceptHashing(HashingVisitor visitor) {
+    // Rather than hash the entire content, hash the sizes and each instruction "type" which
+    // should provide a fast yet reasonably distinct key.
+    visitor.visitInt(instructions.size());
+    visitor.visitInt(tryCatchRanges.size());
+    visitor.visitInt(localVariables.size());
+    instructions.forEach(i -> visitor.visitInt(i.getCompareToId()));
   }
 
-  private static Reference2IntMap<CfLabel> getLabelOrdering(List<CfInstruction> instructions) {
-    Reference2IntMap<CfLabel> ordering = new Reference2IntOpenHashMap<>();
-    for (CfInstruction instruction : instructions) {
-      if (instruction.isLabel()) {
-        ordering.put(instruction.asLabel(), ordering.size());
-      }
-    }
-    return ordering;
+  @Override
+  public void acceptCompareTo(CfCode other, CompareToVisitor visitor) {
+    CfCompareHelper helper = new CfCompareHelper(this, other);
+    visitor.visit(
+        this,
+        other,
+        spec ->
+            spec.withCustomItemCollection(c -> c.instructions, helper.instructionAcceptor())
+                .withCustomItemCollection(c -> c.tryCatchRanges, helper.tryCatchRangeAcceptor())
+                .withCustomItemCollection(c -> c.localVariables, helper.localVariableAcceptor()));
   }
 
   private boolean shouldAddParameterNames(DexEncodedMethod method, AppView<?> appView) {
@@ -758,7 +760,7 @@
               origin, appView.graphLens().getOriginalMethodSignature(method.method), appView),
           appView);
     }
-    DexType context = appView.graphLens().lookupType(method.holder());
+    DexType context = appView.graphLens().lookupType(method.getHolderType());
     DexType returnType = appView.graphLens().lookupType(method.method.getReturnType());
     RewrittenPrototypeDescription rewrittenDescription = RewrittenPrototypeDescription.none();
     if (applyProtoTypeChanges) {
diff --git a/src/main/java/com/android/tools/r8/graph/CfCompareHelper.java b/src/main/java/com/android/tools/r8/graph/CfCompareHelper.java
index b05814e..0203b0c 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCompareHelper.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCompareHelper.java
@@ -6,10 +6,13 @@
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfTryCatch;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAcceptor;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import java.util.Comparator;
-import java.util.List;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import org.objectweb.asm.Opcodes;
 
 public class CfCompareHelper {
@@ -42,35 +45,101 @@
   }
 
   // Helper to signal that the concrete instruction is uniquely determined by its ID/opcode.
-  public static int compareIdUniquelyDeterminesEquality(
+  public static void compareIdUniquelyDeterminesEquality(
       CfInstruction instruction1, CfInstruction instruction2) {
     assert instruction1.getClass() == instruction2.getClass();
     assert instruction1.getCompareToId() == instruction2.getCompareToId();
     assert instruction1.toString().equals(instruction2.toString());
-    return 0;
   }
 
-  private final Reference2IntMap<CfLabel> labels1;
-  private final Reference2IntMap<CfLabel> labels2;
-
-  public CfCompareHelper(Reference2IntMap<CfLabel> labels1, Reference2IntMap<CfLabel> labels2) {
-    this.labels1 = labels1;
-    this.labels2 = labels2;
+  private static Reference2IntMap<CfLabel> getLabelOrdering(CfCode code) {
+    Reference2IntMap<CfLabel> ordering = new Reference2IntOpenHashMap<>();
+    for (CfInstruction instruction : code.getInstructions()) {
+      if (instruction.isLabel()) {
+        ordering.put(instruction.asLabel(), ordering.size());
+      }
+    }
+    return ordering;
   }
 
-  public int compareLabels(CfLabel label1, CfLabel label2) {
-    return labels1.getInt(label1) - labels2.getInt(label2);
+  private final CfCode code1;
+  private final CfCode code2;
+  private StructuralAcceptor<CfLabel> lazyLabelAcceptor = null;
+
+  public CfCompareHelper(CfCode code1, CfCode code2) {
+    this.code1 = code1;
+    this.code2 = code2;
   }
 
-  public Comparator<List<CfInstruction>> instructionComparator() {
-    return ComparatorUtils.listComparator((x, y) -> x.compareTo(y, this));
+  public void compareLabels(CfLabel label1, CfLabel label2, CompareToVisitor visitor) {
+    labelAcceptor().acceptCompareTo(label1, label2, visitor);
   }
 
-  public Comparator<List<CfTryCatch>> tryCatchRangesComparator() {
-    return ComparatorUtils.listComparator((x, y) -> x.compareTo(y, this));
+  public StructuralAcceptor<CfLabel> labelAcceptor() {
+    if (lazyLabelAcceptor == null) {
+      lazyLabelAcceptor =
+          new StructuralAcceptor<CfLabel>() {
+            private final Reference2IntMap<CfLabel> labels1 = getLabelOrdering(code1);
+            private final Reference2IntMap<CfLabel> labels2 = getLabelOrdering(code2);
+
+            @Override
+            public void acceptCompareTo(CfLabel item1, CfLabel item2, CompareToVisitor visitor) {
+              visitor.visitInt(labels1.getInt(item1), labels2.getInt(item2));
+            }
+
+            @Override
+            public void acceptHashing(CfLabel item, HashingVisitor visitor) {
+              throw new Unimplemented();
+            }
+          };
+    }
+    return lazyLabelAcceptor;
   }
 
-  public Comparator<List<CfCode.LocalVariableInfo>> localVariablesComparator() {
-    return ComparatorUtils.listComparator((x, y) -> x.compareTo(y, this));
+  public StructuralAcceptor<CfInstruction> instructionAcceptor() {
+    CfCompareHelper helper = this;
+    return new StructuralAcceptor<CfInstruction>() {
+      @Override
+      public void acceptCompareTo(
+          CfInstruction item1, CfInstruction item2, CompareToVisitor visitor) {
+        item1.acceptCompareTo(item2, visitor, helper);
+      }
+
+      @Override
+      public void acceptHashing(CfInstruction item, HashingVisitor visitor) {
+        throw new Unimplemented();
+      }
+    };
+  }
+
+  public StructuralAcceptor<CfTryCatch> tryCatchRangeAcceptor() {
+    CfCompareHelper helper = this;
+    return new StructuralAcceptor<CfTryCatch>() {
+      @Override
+      public void acceptCompareTo(CfTryCatch item1, CfTryCatch item2, CompareToVisitor visitor) {
+        item1.acceptCompareTo(item2, visitor, helper);
+      }
+
+      @Override
+      public void acceptHashing(CfTryCatch item, HashingVisitor visitor) {
+        throw new Unimplemented();
+      }
+    };
+  }
+
+  public StructuralAcceptor<LocalVariableInfo> localVariableAcceptor() {
+    CfCompareHelper helper = this;
+    return new StructuralAcceptor<LocalVariableInfo>() {
+      @Override
+      public void acceptCompareTo(
+          LocalVariableInfo item1, LocalVariableInfo item2, CompareToVisitor visitor) {
+        item1.acceptCompareTo(item2, visitor, helper);
+      }
+
+      @Override
+      public void acceptHashing(LocalVariableInfo item, HashingVisitor visitor) {
+        throw new Unimplemented();
+      }
+    };
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java b/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
index 333b8a7..56027f2 100644
--- a/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
@@ -5,13 +5,15 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
-import java.util.Comparator;
 
-public class DebugLocalInfo implements Comparable<DebugLocalInfo> {
+public class DebugLocalInfo implements StructuralItem<DebugLocalInfo> {
 
   public enum PrintLevel {
     NONE,
@@ -25,6 +27,12 @@
   public final DexType type;
   public final DexString signature;
 
+  private static void specify(StructuralSpecification<DebugLocalInfo, ?> spec) {
+    spec.withItem(info -> info.name)
+        .withItem(info -> info.type)
+        .withNullableItem(info -> info.signature);
+  }
+
   public DebugLocalInfo(DexString name, DexType type, DexString signature) {
     this.name = name;
     this.type = type;
@@ -32,11 +40,13 @@
   }
 
   @Override
-  public int compareTo(DebugLocalInfo other) {
-    return Comparator.comparing((DebugLocalInfo info) -> info.name)
-        .thenComparing(info -> info.type)
-        .thenComparing(info -> info.signature, Comparator.nullsFirst(DexString::compareTo))
-        .compare(this, other);
+  public DebugLocalInfo self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DebugLocalInfo> getStructuralAccept() {
+    return DebugLocalInfo::specify;
   }
 
   public static boolean localsInfoMapsEqual(
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index 05010c8..5f223f8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -16,6 +16,9 @@
 import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -23,7 +26,7 @@
 import java.util.TreeSet;
 import java.util.function.Function;
 
-public class DexAnnotation extends DexItem {
+public class DexAnnotation extends DexItem implements StructuralItem<DexAnnotation> {
   public static final DexAnnotation[] EMPTY_ARRAY = {};
   public static final int VISIBILITY_BUILD = 0x00;
   public static final int VISIBILITY_RUNTIME = 0x01;
@@ -31,11 +34,25 @@
   public final int visibility;
   public final DexEncodedAnnotation annotation;
 
+  private static void specify(StructuralSpecification<DexAnnotation, ?> spec) {
+    spec.withInt(a -> a.visibility).withItem(a -> a.annotation);
+  }
+
   public DexAnnotation(int visibility, DexEncodedAnnotation annotation) {
     this.visibility = visibility;
     this.annotation = annotation;
   }
 
+  @Override
+  public DexAnnotation self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexAnnotation> getStructuralAccept() {
+    return DexAnnotation::specify;
+  }
+
   public DexType getAnnotationType() {
     return annotation.type;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationElement.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationElement.java
index 237838f..fc764ff 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationElement.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationElement.java
@@ -5,18 +5,35 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 
-public class DexAnnotationElement extends DexItem {
+public class DexAnnotationElement extends DexItem implements StructuralItem<DexAnnotationElement> {
   public static final DexAnnotationElement[] EMPTY_ARRAY = {};
 
   public final DexString name;
   public final DexValue value;
 
+  private static void specify(StructuralSpecification<DexAnnotationElement, ?> spec) {
+    spec.withItem(e -> e.name).withItem(e -> e.value);
+  }
+
   public DexAnnotationElement(DexString name, DexValue value) {
     this.name = name;
     this.value = value;
   }
 
+  @Override
+  public DexAnnotationElement self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexAnnotationElement> getStructuralAccept() {
+    return DexAnnotationElement::specify;
+  }
+
   public DexValue getValue() {
     return value;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
index ba71338..86188d6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
@@ -9,6 +9,9 @@
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.Sets;
 import java.util.Arrays;
 import java.util.List;
@@ -18,7 +21,8 @@
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
-public class DexAnnotationSet extends CachedHashValueDexItem {
+public class DexAnnotationSet extends CachedHashValueDexItem
+    implements StructuralItem<DexAnnotationSet> {
 
   public static final DexAnnotationSet[] EMPTY_ARRAY = {};
 
@@ -29,10 +33,24 @@
   public final DexAnnotation[] annotations;
   private int sorted = UNSORTED;
 
+  private static void specify(StructuralSpecification<DexAnnotationSet, ?> spec) {
+    spec.withItemArray(a -> a.annotations);
+  }
+
   public DexAnnotationSet(DexAnnotation[] annotations) {
     this.annotations = annotations;
   }
 
+  @Override
+  public DexAnnotationSet self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexAnnotationSet> getStructuralAccept() {
+    return DexAnnotationSet::specify;
+  }
+
   public static DexType findDuplicateEntryType(DexAnnotation[] annotations) {
     return findDuplicateEntryType(Arrays.asList(annotations));
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCallSite.java b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
index b38e016..5d521bc 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCallSite.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
@@ -10,6 +10,9 @@
 import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
 import com.android.tools.r8.graph.DexValue.DexValueMethodType;
 import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.io.BaseEncoding;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -22,7 +25,7 @@
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.tree.InvokeDynamicInsnNode;
 
-public final class DexCallSite extends IndexedDexItem implements Comparable<DexCallSite> {
+public final class DexCallSite extends IndexedDexItem implements StructuralItem<DexCallSite> {
 
   public final DexString methodName;
   public final DexProto methodProto;
@@ -30,6 +33,7 @@
   public final DexMethodHandle bootstrapMethod;
   public final List<DexValue> bootstrapArgs;
 
+  // Lazy computed encoding derived from the above immutable fields.
   private DexEncodedArray encodedArray = null;
 
   // Only used for sorting for deterministic output. This is the method and the instruction
@@ -37,6 +41,20 @@
   private DexMethod method;
   private int instructionOffset = -1;
 
+  private static void specify(StructuralSpecification<DexCallSite, ?> spec) {
+    spec
+        // Use the possibly absent "context" info as the major key for sorting.
+        // TODO(b/171867022): Investigate if this is needed now that a call-site can be sorted based
+        //  on its content directly.
+        .withNullableItem(c -> c.method)
+        .withInt(c -> c.instructionOffset)
+        // Actual call-site content.
+        .withItem(c -> c.methodName)
+        .withItem(c -> c.methodProto)
+        .withItem(c -> c.bootstrapMethod)
+        .withItemCollection(c -> c.bootstrapArgs);
+  }
+
   DexCallSite(
       DexString methodName,
       DexProto methodProto,
@@ -84,6 +102,16 @@
     return application.getCallSite(name, desc, bootstrapMethod, bootstrapArgs);
   }
 
+  @Override
+  public DexCallSite self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexCallSite> getStructuralAccept() {
+    return DexCallSite::specify;
+  }
+
   public void setContext(DexMethod method, int instructionOffset) {
     assert method != null;
     assert instructionOffset >= 0;
@@ -155,17 +183,6 @@
     return new HashBuilder().build();
   }
 
-  @Override
-  public int compareTo(DexCallSite other) {
-    assert method != null && other.method != null;
-    int methodCompare = method.compareTo(other.method);
-    if (methodCompare != 0) {
-      return methodCompare;
-    }
-    assert (instructionOffset - other.instructionOffset) != 0;
-    return instructionOffset - other.instructionOffset;
-  }
-
   private final class HashBuilder {
     private ByteArrayOutputStream bytes;
     private ObjectOutputStream out;
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 5215ada..be3a802 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -130,6 +130,10 @@
     return interfaces;
   }
 
+  public void setInterfaces(DexTypeList interfaces) {
+    this.interfaces = interfaces;
+  }
+
   public DexString getSourceFile() {
     return sourceFile;
   }
@@ -378,7 +382,7 @@
   }
 
   private boolean verifyCorrectnessOfFieldHolder(DexEncodedField field) {
-    assert field.holder() == type
+    assert field.getHolderType() == type
         : "Expected field `"
             + field.field.toSourceString()
             + "` to have holder `"
@@ -495,7 +499,8 @@
   }
 
   private boolean isSignaturePolymorphicMethod(DexEncodedMethod method, DexItemFactory factory) {
-    assert method.holder() == factory.methodHandleType || method.holder() == factory.varHandleType;
+    assert method.getHolderType() == factory.methodHandleType
+        || method.getHolderType() == factory.varHandleType;
     return method.accessFlags.isVarargs()
         && method.accessFlags.isNative()
         && method.method.proto.parameters.size() == 1
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java b/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
index ddb14ec..fbd5e7a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
@@ -16,7 +16,7 @@
   public DexClassAndMember(DexClass holder, D definition) {
     assert holder != null;
     assert definition != null;
-    assert holder.type == definition.holder();
+    assert holder.type == definition.getHolderType();
     this.holder = holder;
     this.definition = definition;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
index e73c267..32743f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
@@ -73,7 +73,7 @@
     int argumentRegister = code.registerSize - code.incomingRegisterSize;
     if (!method.accessFlags.isStatic()) {
       DexString name = factory.thisName;
-      DexType type = method.holder();
+      DexType type = method.getHolderType();
       startArgument(argumentRegister, name, type);
       argumentRegister += ValueType.fromDexType(type).requiredRegisters();
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
index 051dd92..bcefcd2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
@@ -6,11 +6,14 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Arrays;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-public class DexEncodedAnnotation extends DexItem {
+public class DexEncodedAnnotation extends DexItem implements StructuralItem<DexEncodedAnnotation> {
 
   private static final int UNSORTED = 0;
 
@@ -19,11 +22,25 @@
 
   private int sorted = UNSORTED;
 
+  private static void specify(StructuralSpecification<DexEncodedAnnotation, ?> spec) {
+    spec.withItem(a -> a.type).withItemArray(a -> a.elements);
+  }
+
   public DexEncodedAnnotation(DexType type, DexAnnotationElement[] elements) {
     this.type = type;
     this.elements = elements;
   }
 
+  @Override
+  public DexEncodedAnnotation self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexEncodedAnnotation> getStructuralAccept() {
+    return DexEncodedAnnotation::specify;
+  }
+
   public void collectIndexedItems(IndexedItemCollection indexedItems) {
     type.collectIndexedItems(indexedItems);
     for (DexAnnotationElement element : elements) {
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 16879f0..133ebe5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -20,8 +20,12 @@
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
 import com.android.tools.r8.kotlin.KotlinFieldLevelInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 
-public class DexEncodedField extends DexEncodedMember<DexEncodedField, DexField> {
+public class DexEncodedField extends DexEncodedMember<DexEncodedField, DexField>
+    implements StructuralItem<DexEncodedField> {
   public static final DexEncodedField[] EMPTY_ARRAY = {};
 
   public final DexField field;
@@ -34,6 +38,16 @@
   private FieldOptimizationInfo optimizationInfo = DefaultFieldOptimizationInfo.getInstance();
   private KotlinFieldLevelInfo kotlinMemberInfo = NO_KOTLIN_INFO;
 
+  private static void specify(StructuralSpecification<DexEncodedField, ?> spec) {
+    spec.withItem(f -> f.field)
+        .withItem(f -> f.accessFlags)
+        .withNullableItem(f -> f.staticValue)
+        .withBool(f -> f.deprecated)
+        // TODO(b/171867022): The generic signature should be part of the definition.
+        .withAssert(f -> f.genericSignature.hasNoSignature());
+    // TODO(b/171867022): Should the optimization info and member info be part of the definition?
+  }
+
   public DexEncodedField(
       DexField field,
       FieldAccessFlags accessFlags,
@@ -60,6 +74,16 @@
     this(field, accessFlags, genericSignature, annotations, staticValue, false);
   }
 
+  @Override
+  public StructuralAccept<DexEncodedField> getStructuralAccept() {
+    return DexEncodedField::specify;
+  }
+
+  @Override
+  public DexEncodedField self() {
+    return this;
+  }
+
   public DexType type() {
     return field.type;
   }
@@ -150,7 +174,7 @@
   }
 
   public ProgramField asProgramField(DexDefinitionSupplier definitions) {
-    assert holder().isClassType();
+    assert getHolderType().isClassType();
     DexProgramClass clazz = asProgramClassOrNull(definitions.definitionForHolder(field));
     if (clazz != null) {
       return new ProgramField(clazz, this);
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 dd69340..bca8a6b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -12,11 +12,15 @@
     super(annotations);
   }
 
-  public DexType holder() {
-    return getReference().holder;
+  public abstract KotlinMemberLevelInfo getKotlinMemberInfo();
+
+  public DexType getHolderType() {
+    return getReference().getHolderType();
   }
 
-  public abstract KotlinMemberLevelInfo getKotlinMemberInfo();
+  public DexString getName() {
+    return getReference().getName();
+  }
 
   @Override
   public abstract R getReference();
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 e84e3e8..6fbb596 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -75,7 +75,9 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.CompareToVisitorWithTypeEquivalence;
+import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitorWithTypeEquivalence;
 import com.android.tools.r8.utils.structural.Ordered;
 import com.android.tools.r8.utils.structural.RepresentativeMap;
@@ -325,17 +327,19 @@
   // Visitor specifying the structure of the method with respect to its "synthetic" content.
   // TODO(b/171867022): Generalize this so that it determines any method in full.
   private static void syntheticSpecify(StructuralSpecification<DexEncodedMethod, ?> spec) {
-    spec.withAssert(m1 -> m1.annotations().isEmpty())
-        .withAssert(m1 -> m1.parameterAnnotationsList.isEmpty())
+    spec.withItem(m -> m.method)
+        .withItem(m -> m.accessFlags)
+        .withItem(m -> m.annotations())
+        .withItem(m -> m.parameterAnnotationsList)
+        .withNullableItem(m -> m.classFileVersion)
+        .withBool(m -> m.d8R8Synthesized)
+        // TODO(b/171867022): Make signatures structural and include it in the definition.
+        .withAssert(m -> m.genericSignature.hasNoSignature())
         .withAssert(m1 -> m1.code != null)
-        .withItem(DexEncodedMethod::getHolderType)
-        .withItem(DexEncodedMethod::getName)
-        .withInt(m -> m.getAccessFlags().getAsCfAccessFlags())
-        .withItem(DexEncodedMethod::proto)
         .withCustomItem(
             DexEncodedMethod::getCode,
-            (c1, c2, v) -> v.visit(c1, c2, DexEncodedMethod::compareCodeObject),
-            (c, h) -> h.visit(c, DexEncodedMethod::hashCodeObject));
+            DexEncodedMethod::compareCodeObject,
+            DexEncodedMethod::hashCodeObject);
   }
 
   public void hashSyntheticContent(Hasher hasher, RepresentativeMap map) {
@@ -354,37 +358,28 @@
         this, other, map, DexEncodedMethod::syntheticSpecify);
   }
 
-  private static int compareCodeObject(Code code1, Code code2) {
+  private static void compareCodeObject(Code code1, Code code2, CompareToVisitor visitor) {
     if (code1.isCfCode() && code2.isCfCode()) {
-      return code1.asCfCode().compareTo(code2.asCfCode());
-    }
-    if (code1.isDexCode() && code2.isDexCode()) {
-      return code1.asDexCode().compareTo(code2.asDexCode());
-    }
-    throw new Unreachable(
-        "Unexpected attempt to compare incompatible synthetic objects: " + code1 + " and " + code2);
-  }
-
-  private static void hashCodeObject(Code code, Hasher hasher) {
-    // TODO(b/158159959): Implement a more precise hashing on code objects.
-    if (code.isCfCode()) {
-      CfCode cfCode = code.asCfCode();
-      hasher.putInt(cfCode.getInstructions().size());
-      for (CfInstruction instruction : cfCode.getInstructions()) {
-        hasher.putInt(instruction.getClass().hashCode());
-      }
+      code1.asCfCode().acceptCompareTo(code2.asCfCode(), visitor);
+    } else if (code1.isDexCode() && code2.isDexCode()) {
+      visitor.visit(code1.asDexCode(), code2.asDexCode(), DexCode::compareTo);
     } else {
-      assert code.isDexCode();
-      hasher.putInt(code.hashCode());
+      throw new Unreachable(
+          "Unexpected attempt to compare incompatible synthetic objects: "
+              + code1
+              + " and "
+              + code2);
     }
   }
 
-  public DexType getHolderType() {
-    return getReference().holder;
-  }
-
-  public DexString getName() {
-    return getReference().name;
+  private static void hashCodeObject(Code code, HashingVisitor visitor) {
+    if (code.isCfCode()) {
+      code.asCfCode().acceptHashing(visitor);
+    } else {
+      // TODO(b/158159959): Implement a more precise hashing on code objects.
+      assert code.isDexCode();
+      visitor.visitInt(code.hashCode());
+    }
   }
 
   public DexProto getProto() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 5f95369..dbb2d08 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -42,14 +42,6 @@
     return DexField::accept;
   }
 
-  public DexType getHolderType() {
-    return holder;
-  }
-
-  public DexString getName() {
-    return name;
-  }
-
   public DexType getType() {
     return type;
   }
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 eee5f4b..dac5b16 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.LRUCacheTable;
 import com.google.common.base.Strings;
 import com.google.common.collect.BiMap;
@@ -37,6 +38,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -46,9 +48,12 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
@@ -1828,6 +1833,19 @@
     return createGloballyFreshMemberString(baseName, null);
   }
 
+  public DexType createFreshTypeName(DexType type, Predicate<DexType> isFresh) {
+    return createFreshTypeName(type, isFresh, 0);
+  }
+
+  public DexType createFreshTypeName(DexType type, Predicate<DexType> isFresh, int index) {
+    while (true) {
+      DexType newType = type.addSuffixId(index++, this);
+      if (isFresh.test(newType)) {
+        return newType;
+      }
+    }
+  }
+
   /**
    * Tries to find a method name for insertion into the class {@code target} of the form
    * baseName$holder$n, where {@code baseName} and {@code holder} are supplied by the user, and
@@ -1909,22 +1927,45 @@
   }
 
   public DexMethod createInstanceInitializerWithFreshProto(
+      DexMethod method, List<DexType> extraTypes, Predicate<DexMethod> isFresh) {
+    assert method.isInstanceInitializer(this);
+    return createInstanceInitializerWithFreshProto(
+        method.proto,
+        extraTypes,
+        proto -> Optional.of(method.withProto(proto, this)).filter(isFresh));
+  }
+
+  public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, DexType extraType, Predicate<DexMethod> isFresh) {
     assert method.isInstanceInitializer(this);
     return createInstanceInitializerWithFreshProto(
         method.proto,
-        extraType,
-        proto -> Optional.of(createMethod(method.holder, proto, method.name)).filter(isFresh));
+        ImmutableList.of(extraType),
+        proto -> Optional.of(method.withProto(proto, this)).filter(isFresh));
   }
 
   private DexMethod createInstanceInitializerWithFreshProto(
-      DexProto proto, DexType extraType, Function<DexProto, Optional<DexMethod>> isFresh) {
+      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();
+
+    int count = 0;
     while (true) {
-      Optional<DexMethod> object = isFresh.apply(proto);
+      assert count++ < 100;
+      if (!current.hasNext()) {
+        assert !tryProtos.isEmpty();
+        current = tryProtos.remove().iterator();
+        assert current.hasNext();
+      }
+      DexProto tryProto = current.next();
+      Optional<DexMethod> object = isFresh.apply(tryProto);
       if (object.isPresent()) {
         return object.get();
       }
-      proto = appendTypeToProto(proto, extraType);
+      tryProtos.add(
+          Iterables.transform(extraTypes, extraType -> appendTypeToProto(tryProto, extraType)));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexMember.java b/src/main/java/com/android/tools/r8/graph/DexMember.java
index 84a1953..790fd33 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMember.java
@@ -31,6 +31,14 @@
     return holder;
   }
 
+  public DexType getHolderType() {
+    return holder;
+  }
+
+  public DexString getName() {
+    return name;
+  }
+
   @Override
   public boolean isDexMember() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 249e77e..97f9b6d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -50,14 +50,6 @@
     visitor.visitDexMethod(this, other);
   }
 
-  public DexType getHolderType() {
-    return holder;
-  }
-
-  public DexString getName() {
-    return name;
-  }
-
   public DexType getParameter(int index) {
     return proto.getParameter(index);
   }
@@ -276,4 +268,8 @@
   public DexMethod withName(DexString name, DexItemFactory dexItemFactory) {
     return dexItemFactory.createMethod(holder, proto, name);
   }
+
+  public DexMethod withProto(DexProto proto, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createMethod(holder, proto, name);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexReference.java b/src/main/java/com/android/tools/r8/graph/DexReference.java
index d32c7ba..8a59614 100644
--- a/src/main/java/com/android/tools/r8/graph/DexReference.java
+++ b/src/main/java/com/android/tools/r8/graph/DexReference.java
@@ -63,7 +63,7 @@
     return null;
   }
 
-  private int referenceTypeOrder() {
+  public int referenceTypeOrder() {
     if (isDexType()) {
       return 1;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 7a50fe1..264a7aa 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -46,9 +46,15 @@
     throw new Unreachable();
   }
 
+  /** DexString is a leaf item so we directly define its compareTo which avoids overhead. */
+  @Override
+  public int compareTo(DexString other) {
+    return internalCompareTo(other);
+  }
+
   @Override
   public void acceptCompareTo(DexString other, CompareToVisitor visitor) {
-    visitor.visitDexString(this, other, DexString::internalCompareTo);
+    visitor.visitDexString(this, other);
   }
 
   @Override
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 39aea8c..0b13d80 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -444,6 +444,14 @@
     return dexItemFactory.createType(newDescriptorString);
   }
 
+  public DexType addSuffixId(int index, DexItemFactory dexItemFactory) {
+    if (index == 0) {
+      return this;
+    }
+    assert index > 0;
+    return addSuffix("$" + index, dexItemFactory);
+  }
+
   public DexType toArrayType(int dimensions, DexItemFactory dexItemFactory) {
     return dexItemFactory.createType(descriptor.toArrayDescriptor(dimensions, dexItemFactory));
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeList.java b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
index a641039..595c5dd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
@@ -3,18 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.utils.PredicateUtils.not;
+
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.ArrayUtils;
-import com.android.tools.r8.utils.structural.CompareToVisitor;
-import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralAccept;
 import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.Iterators;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 public class DexTypeList extends DexItem implements Iterable<DexType>, StructuralItem<DexTypeList> {
@@ -23,6 +25,10 @@
 
   public final DexType[] values;
 
+  private static void specify(StructuralSpecification<DexTypeList, ?> spec) {
+    spec.withItemArray(ts -> ts.values);
+  }
+
   public static DexTypeList empty() {
     return theEmptyTypeList;
   }
@@ -36,10 +42,28 @@
     this.values = values;
   }
 
-  @Override
-  public StructuralAccept<DexTypeList> getStructuralAccept() {
-    // Structural accept is never accessed as all accept methods are defined directly.
-    throw new Unreachable();
+  public DexTypeList(Collection<DexType> values) {
+    this(values.toArray(DexType.EMPTY_ARRAY));
+  }
+
+  public static DexTypeList create(DexType[] values) {
+    return values.length == 0 ? DexTypeList.empty() : new DexTypeList(values);
+  }
+
+  public static DexTypeList create(Collection<DexType> values) {
+    return values.isEmpty() ? DexTypeList.empty() : new DexTypeList(values);
+  }
+
+  public DexTypeList keepIf(Predicate<DexType> predicate) {
+    DexType[] filtered = ArrayUtils.filter(DexType[].class, values, predicate);
+    if (filtered != values) {
+      return DexTypeList.create(filtered);
+    }
+    return this;
+  }
+
+  public DexTypeList removeIf(Predicate<DexType> predicate) {
+    return keepIf(not(predicate));
   }
 
   @Override
@@ -48,13 +72,8 @@
   }
 
   @Override
-  public void acceptCompareTo(DexTypeList other, CompareToVisitor visitor) {
-    visitor.visitDexTypeList(this, other);
-  }
-
-  @Override
-  public void acceptHashing(HashingVisitor visitor) {
-    visitor.visitDexTypeList(this);
+  public StructuralAccept<DexTypeList> getStructuralAccept() {
+    return DexTypeList::specify;
   }
 
   public boolean contains(DexType type) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 273c69a..a54daf8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -23,12 +23,16 @@
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.EncodedValueUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
 import java.util.Arrays;
 import java.util.function.Consumer;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Type;
 
-public abstract class DexValue extends DexItem {
+public abstract class DexValue extends DexItem implements StructuralItem<DexValue> {
 
   public enum DexValueKind {
     BYTE(0x00),
@@ -104,6 +108,41 @@
     }
   }
 
+  @Override
+  public DexValue self() {
+    return this;
+  }
+
+  @Override
+  public final StructuralAccept<DexValue> getStructuralAccept() {
+    // DexValue is not generic at its base type (and can't as we use it as a polymorphic value),
+    // so each concrete value must implement polymorphic accept functions. This base class
+    // implements (most of) the polymorphic checks and concrete types implement the internal
+    // variants for that type.
+    throw new Unreachable();
+  }
+
+  @Override
+  public final void acceptCompareTo(DexValue other, CompareToVisitor visitor) {
+    // Order first on 'kind', only equal kinds then forward to the 'kind' specific internal compare.
+    if (getValueKind() != other.getValueKind()) {
+      visitor.visitInt(getValueKind().toByte(), other.getValueKind().toByte());
+    } else {
+      internalAcceptCompareTo(other, visitor);
+    }
+  }
+
+  @Override
+  public final void acceptHashing(HashingVisitor visitor) {
+    // Always hash the 'kind' which ensures that raw values of different type are distinct.
+    visitor.visitInt(getValueKind().toByte());
+    internalAcceptHashing(visitor);
+  }
+
+  abstract void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor);
+
+  abstract void internalAcceptHashing(HashingVisitor visitor);
+
   public static final DexValue[] EMPTY_ARRAY = {};
 
   public abstract DexValueKind getValueKind();
@@ -438,6 +477,16 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueByte(value);
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitInt(value, other.asDexValueByte().value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
+    }
+
     public byte getValue() {
       return value;
     }
@@ -521,6 +570,16 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueShort(value);
     }
 
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitInt(value, other.asDexValueShort().getValue());
+    }
+
     public short getValue() {
       return value;
     }
@@ -603,6 +662,16 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueChar(value);
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitInt(value, other.asDexValueChar().value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
+    }
+
     public char getValue() {
       return value;
     }
@@ -689,6 +758,16 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueInt(value);
     }
 
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitInt(value, other.asDexValueInt().value);
+    }
+
     public int getValue() {
       return value;
     }
@@ -771,6 +850,16 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueLong(value);
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitLong(value, other.asDexValueLong().value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitLong(value);
+    }
+
     public long getValue() {
       return value;
     }
@@ -853,6 +942,16 @@
       return Float.compare(value, DEFAULT.value) == 0 ? DEFAULT : new DexValueFloat(value);
     }
 
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitFloat(value);
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitFloat(value, other.asDexValueFloat().value);
+    }
+
     public float getValue() {
       return value;
     }
@@ -941,6 +1040,16 @@
       return Double.compare(value, DEFAULT.value) == 0 ? DEFAULT : new DexValueDouble(value);
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitDouble(value, other.asDexValueDouble().value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitDouble(value);
+    }
+
     public double getValue() {
       return value;
     }
@@ -1087,6 +1196,18 @@
     }
 
     @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      if (DexItemBasedValueString.compareAndCheckValueStrings(this, other, visitor)) {
+        value.acceptCompareTo(other.asDexValueString().value, visitor);
+      }
+    }
+
+    @Override
     public void collectIndexedItems(IndexedItemCollection indexedItems) {
       value.collectIndexedItems(indexedItems);
     }
@@ -1143,6 +1264,20 @@
 
   public static class DexItemBasedValueString extends NestedDexValue<DexReference> {
 
+    // Helper to ensure a consistent order on DexValueString and DexItemBasedValueString which are
+    // both defined to have kind 'string'.
+    static boolean compareAndCheckValueStrings(DexValue v1, DexValue v2, CompareToVisitor visitor) {
+      assert v1.getValueKind() == DexValueKind.STRING;
+      assert v2.getValueKind() == DexValueKind.STRING;
+      int order1 = v1.isDexItemBasedValueString() ? 1 : 0;
+      int order2 = v2.isDexItemBasedValueString() ? 1 : 0;
+      boolean equal = order1 == order2;
+      if (!equal) {
+        visitor.visitInt(order1, order2);
+      }
+      return equal;
+    }
+
     private final NameComputationInfo<?> nameComputationInfo;
 
     public DexItemBasedValueString(DexReference value, NameComputationInfo<?> nameComputationInfo) {
@@ -1151,6 +1286,18 @@
     }
 
     @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitDexReference(value);
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      if (compareAndCheckValueStrings(this, other, visitor)) {
+        visitor.visitDexReference(value, other.asDexItemBasedValueString().value);
+      }
+    }
+
+    @Override
     public void collectIndexedItems(IndexedItemCollection indexedItems) {
       value.collectIndexedItems(indexedItems);
     }
@@ -1221,6 +1368,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueType().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public DexValueKind getValueKind() {
       return DexValueKind.TYPE;
     }
@@ -1253,6 +1410,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueField().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public DexValueKind getValueKind() {
       return DexValueKind.FIELD;
     }
@@ -1285,6 +1452,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueMethod().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public DexValueKind getValueKind() {
       return DexValueKind.METHOD;
     }
@@ -1317,6 +1494,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueEnum().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public DexValueKind getValueKind() {
       return DexValueKind.ENUM;
     }
@@ -1349,6 +1536,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueMethodType().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public boolean isDexValueMethodType() {
       return true;
     }
@@ -1382,6 +1579,16 @@
       this.values = values;
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitItemArray(values, other.asDexValueArray().values);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitItemArray(values);
+    }
+
     public void forEachElement(Consumer<DexValue> consumer) {
       for (DexValue value : values) {
         consumer.accept(value);
@@ -1481,6 +1688,16 @@
       this.value = value;
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueAnnotation().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
     public DexEncodedAnnotation getValue() {
       return value;
     }
@@ -1567,6 +1784,17 @@
     private DexValueNull() {
     }
 
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      assert this == NULL;
+    }
+
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      assert this == NULL;
+      assert other == NULL;
+    }
+
     public Object getValue() {
       return null;
     }
@@ -1653,6 +1881,16 @@
       return value ? TRUE : FALSE;
     }
 
+    @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      visitor.visitBool(value, other.asDexValueBoolean().value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitBool(value);
+    }
+
     public boolean getValue() {
       return value;
     }
@@ -1729,6 +1967,16 @@
     }
 
     @Override
+    void internalAcceptCompareTo(DexValue other, CompareToVisitor visitor) {
+      value.acceptCompareTo(other.asDexValueMethodHandle().value, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      value.acceptHashing(visitor);
+    }
+
+    @Override
     public boolean isDexValueMethodHandle() {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
index b3f9a6f..4483fc5 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
@@ -60,7 +60,7 @@
 
     SuccessfulFieldResolutionResult(
         DexClass initialResolutionHolder, DexClass resolvedHolder, DexEncodedField resolvedField) {
-      assert resolvedHolder.type == resolvedField.holder();
+      assert resolvedHolder.type == resolvedField.getHolderType();
       this.initialResolutionHolder = initialResolutionHolder;
       this.resolvedHolder = resolvedHolder;
       this.resolvedField = resolvedField;
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index 398ed87..5a10844 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -14,6 +14,8 @@
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -229,7 +231,8 @@
     protected final Map<DexField, DexField> fieldMap = new IdentityHashMap<>();
 
     protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
 
     public void map(DexType from, DexType to) {
       if (from == to) {
@@ -965,7 +968,8 @@
     // Maps that store the original signature of fields and methods that have been affected, for
     // example, by vertical class merging. Needed to generate a correct Proguard map in the end.
     protected final BiMap<DexField, DexField> originalFieldSignatures;
-    protected BiMap<DexMethod, DexMethod> originalMethodSignatures;
+    protected BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod>
+        originalMethodSignatures;
 
     // Overrides this if the sub type needs to be a nested lens while it doesn't have any mappings
     // at all, e.g., publicizer lens that changes invocation type only.
@@ -978,7 +982,7 @@
         Map<DexMethod, DexMethod> methodMap,
         Map<DexField, DexField> fieldMap,
         BiMap<DexField, DexField> originalFieldSignatures,
-        BiMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
       super(dexItemFactory, previousLens);
@@ -1138,15 +1142,11 @@
 
     @Override
     protected DexMethod internalGetPreviousMethodSignature(DexMethod method) {
-      return originalMethodSignatures != null
-          ? originalMethodSignatures.getOrDefault(method, method)
-          : method;
+      return originalMethodSignatures.getRepresentativeValueOrDefault(method, method);
     }
 
     protected DexMethod internalGetNextMethodSignature(DexMethod method) {
-      return originalMethodSignatures != null
-          ? originalMethodSignatures.inverse().getOrDefault(method, method)
-          : method;
+      return originalMethodSignatures.getRepresentativeKeyOrDefault(method, method);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/MethodCollection.java b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
index 3200163..9339630 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -342,7 +342,7 @@
   }
 
   private boolean verifyCorrectnessOfMethodHolder(DexEncodedMethod method) {
-    assert method.holder() == holder.type
+    assert method.getHolderType() == holder.type
         : "Expected method `"
             + method.method.toSourceString()
             + "` to have holder `"
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
index b5ce56a..61d9439 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
@@ -7,6 +7,9 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.structural.CompareToVisitorWithStringTable;
+import com.android.tools.r8.utils.structural.CompareToVisitorWithTypeTable;
 import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap.Entry;
@@ -17,6 +20,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.function.ToIntFunction;
 import java.util.stream.Collectors;
 
 public class ObjectToOffsetMapping {
@@ -53,7 +57,8 @@
       Collection<DexField> fields,
       Collection<DexString> strings,
       Collection<DexCallSite> callSites,
-      Collection<DexMethodHandle> methodHandles) {
+      Collection<DexMethodHandle> methodHandles,
+      Timing timing) {
     assert appView != null;
     assert graphLens != null;
     assert classes != null;
@@ -69,18 +74,42 @@
     this.namingLens = namingLens;
     this.initClassLens = initClassLens;
     this.lensCodeRewriter = new LensCodeRewriterUtils(appView);
+    timing.begin("Sort strings");
+    this.strings = createSortedMap(strings, DexString::compareTo, this::setFirstJumboString);
+    timing.end();
+    timing.begin("Sort types");
+    this.types = createSortedMap(types, compareWithStringTable(), this::failOnOverflow);
+    timing.end();
+    timing.begin("Sort classes");
     this.classes = sortClasses(appView.appInfo(), classes, namingLens);
-    this.protos = createSortedMap(protos, compare(namingLens), this::failOnOverflow);
-    this.types = createSortedMap(types, compare(namingLens), this::failOnOverflow);
-    this.methods = createSortedMap(methods, compare(namingLens), this::failOnOverflow);
-    this.fields = createSortedMap(fields, compare(namingLens), this::failOnOverflow);
-    this.strings = createSortedMap(strings, compare(namingLens), this::setFirstJumboString);
+    timing.end();
+    timing.begin("Sort protos");
+    this.protos = createSortedMap(protos, compareWithTypeTable(), this::failOnOverflow);
+    timing.end();
+    timing.begin("Sort methods");
+    this.methods = createSortedMap(methods, compareWithTypeTable(), this::failOnOverflow);
+    timing.end();
+    timing.begin("Sort fields");
+    this.fields = createSortedMap(fields, compareWithTypeTable(), this::failOnOverflow);
+    timing.end();
+    timing.begin("Sort call-sites");
     this.callSites = createSortedMap(callSites, DexCallSite::compareTo, this::failOnOverflow);
-    this.methodHandles = createSortedMap(methodHandles, compare(namingLens), this::failOnOverflow);
+    timing.end();
+    timing.begin("Sort method handles");
+    this.methodHandles =
+        createSortedMap(methodHandles, compareWithTypeTable(), this::failOnOverflow);
+    timing.end();
   }
 
-  private static <T extends NamingLensComparable<T>> Comparator<T> compare(NamingLens namingLens) {
-    return (a, b) -> a.compareToWithNamingLens(b, namingLens);
+  private <T extends NamingLensComparable<T>> Comparator<T> compareWithStringTable() {
+    return (a, b) ->
+        CompareToVisitorWithStringTable.run(
+            a, b, namingLens, (ToIntFunction<DexString>) strings::getInt);
+  }
+
+  private <T extends NamingLensComparable<T>> Comparator<T> compareWithTypeTable() {
+    return (a, b) ->
+        CompareToVisitorWithTypeTable.run(a, b, namingLens, strings::getInt, types::getInt);
   }
 
   private void setFirstJumboString(DexString string) {
@@ -95,7 +124,7 @@
   private <T> Reference2IntLinkedOpenHashMap<T> createSortedMap(
       Collection<T> items, Comparator<T> comparator, Consumer<T> onUInt16Overflow) {
     if (items.isEmpty()) {
-      return null;
+      return new Reference2IntLinkedOpenHashMap<>();
     }
     // Sort items and compute the offset mapping for each in sorted order.
     ArrayList<T> sorted = new ArrayList<>(items);
diff --git a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
index 4b21c2b..fb642d6 100644
--- a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
+++ b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
@@ -6,6 +6,9 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Arrays;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -35,7 +38,8 @@
  * ParameterAnnotationsList#isMissing(int)} accessor is used to determine whether a given parameter
  * is missing in the ParameterAnnotations attribute.
  */
-public class ParameterAnnotationsList extends DexItem {
+public class ParameterAnnotationsList extends DexItem
+    implements StructuralItem<ParameterAnnotationsList> {
 
   private static final ParameterAnnotationsList EMPTY_PARAMETER_ANNOTATIONS_LIST =
       new ParameterAnnotationsList();
@@ -43,6 +47,10 @@
   private final DexAnnotationSet[] values;
   private final int missingParameterAnnotations;
 
+  private static void specify(StructuralSpecification<ParameterAnnotationsList, ?> spec) {
+    spec.withItemArray(a -> a.values).withInt(a -> a.missingParameterAnnotations);
+  }
+
   public static ParameterAnnotationsList empty() {
     return EMPTY_PARAMETER_ANNOTATIONS_LIST;
   }
@@ -62,6 +70,16 @@
     this.missingParameterAnnotations = missingParameterAnnotations;
   }
 
+  @Override
+  public ParameterAnnotationsList self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<ParameterAnnotationsList> getStructuralAccept() {
+    return ParameterAnnotationsList::specify;
+  }
+
   public int getAnnotableParameterCount() {
     return size();
   }
@@ -77,6 +95,7 @@
       return true;
     }
     if (other instanceof ParameterAnnotationsList) {
+      // TODO(b/172999904): Why does equals not include missingParameterAnnotations?
       return Arrays.equals(values, ((ParameterAnnotationsList) other).values);
     }
     return false;
diff --git a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
index 4db00fc..09533b0 100644
--- a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
@@ -136,12 +136,12 @@
       assert initialResolutionHolder != null;
       assert resolvedHolder != null;
       assert resolvedMethod != null;
-      assert resolvedHolder.type == resolvedMethod.holder();
+      assert resolvedHolder.type == resolvedMethod.getHolderType();
       this.resolvedHolder = resolvedHolder;
       this.resolvedMethod = resolvedMethod;
       this.initialResolutionHolder = initialResolutionHolder;
       assert !resolvedMethod.isPrivateMethod()
-          || initialResolutionHolder.type == resolvedMethod.holder();
+          || initialResolutionHolder.type == resolvedMethod.getHolderType();
     }
 
     @Override
@@ -370,7 +370,7 @@
       // It appears as if this check is also in place for non-initializer methods too.
       // See NestInvokeSpecialMethodAccessWithIntermediateTest.
       if ((target.isInstanceInitializer() || target.isPrivateMethod())
-          && target.holder() != symbolicReference.type) {
+          && target.getHolderType() != symbolicReference.type) {
         return null;
       }
       // Runtime exceptions:
@@ -610,7 +610,7 @@
         }
         if (candidate == null || candidate == DexEncodedMethod.SENTINEL) {
           // We cannot find a target above the resolved method.
-          if (current.type == overrideTarget.holder()) {
+          if (current.type == overrideTarget.getHolderType()) {
             return null;
           }
           current = current.superType == null ? null : appInfo.definitionFor(current.superType);
@@ -689,7 +689,7 @@
       }
       // For package private methods, a valid override has to be inside the package.
       assert resolvedMethod.accessFlags.isPackagePrivate();
-      return resolvedMethod.holder().isSamePackage(candidate.holder());
+      return resolvedMethod.getHolderType().isSamePackage(candidate.getHolderType());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index 8a44fd0..c8fc0a8 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -396,6 +396,12 @@
     return rewrittenReturnInfo != null;
   }
 
+  public boolean requiresRewritingAtCallSite() {
+    return hasRewrittenReturnInfo()
+        || numberOfExtraParameters() > 0
+        || argumentInfoCollection.numberOfRemovedArguments() > 0;
+  }
+
   public RewrittenTypeInfo getRewrittenReturnInfo() {
     return rewrittenReturnInfo;
   }
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 7cf3029..26ed21c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -25,9 +25,10 @@
   private final Builder lensBuilder;
 
   public ClassInstanceFieldsMerger(
-      HorizontalClassMergerGraphLens.Builder lensBuilder, DexProgramClass target) {
+      HorizontalClassMergerGraphLens.Builder lensBuilder, MergeGroup group) {
     this.lensBuilder = lensBuilder;
-    target
+    group
+        .getTarget()
         .instanceFields()
         .forEach(field -> fieldMappings.computeIfAbsent(field, ignore -> new ArrayList<>()));
   }
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 f75f969..04b4b68 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -11,12 +11,12 @@
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -25,26 +25,28 @@
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
- * The class merger is responsible for moving methods from {@link ClassMerger#toMergeGroup} into the
- * class {@link ClassMerger#target}. While performing merging, this class tracks which methods have
- * been moved, as well as which fields have been remapped in the {@link ClassMerger#lensBuilder}.
+ * The class merger is responsible for moving methods from the sources in {@link ClassMerger#group}
+ * into the target of {@link ClassMerger#group}. While performing merging, this class tracks which
+ * methods have been moved, as well as which fields have been remapped in the {@link
+ * ClassMerger#lensBuilder}.
  */
 public class ClassMerger {
+
   public static final String CLASS_ID_FIELD_NAME = "$r8$classId";
 
-  private final DexProgramClass target;
-  private final List<DexProgramClass> toMergeGroup;
+  private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
   private final HorizontallyMergedClasses.Builder mergedClassesBuilder;
@@ -56,58 +58,41 @@
   private final ClassInstanceFieldsMerger classInstanceFieldsMerger;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
   private final Collection<ConstructorMerger> constructorMergers;
-  private final DexField classIdField;
 
   private ClassMerger(
       AppView<AppInfoWithLiveness> appView,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       HorizontallyMergedClasses.Builder mergedClassesBuilder,
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
-      DexProgramClass target,
-      List<DexProgramClass> toMergeGroup,
-      DexField classIdField,
+      MergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers,
       Collection<ConstructorMerger> constructorMergers) {
     this.lensBuilder = lensBuilder;
     this.mergedClassesBuilder = mergedClassesBuilder;
     this.fieldAccessChangesBuilder = fieldAccessChangesBuilder;
-    this.target = target;
-    this.toMergeGroup = toMergeGroup;
-    this.classIdField = classIdField;
+    this.group = group;
     this.virtualMethodMergers = virtualMethodMergers;
     this.constructorMergers = constructorMergers;
 
     this.dexItemFactory = appView.dexItemFactory();
-    this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, target);
-    this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(lensBuilder, target);
+    this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, group);
+    this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(lensBuilder, group);
 
     buildClassIdentifierMap();
   }
 
-  /** Returns an iterable over all classes that should be merged into the target class. */
-  public Iterable<DexProgramClass> getToMergeClasses() {
-    return toMergeGroup;
-  }
-
-  /**
-   * Returns an iterable over both the target class as well as all classes that should be merged
-   * into the target class.
-   */
-  public Iterable<DexProgramClass> getClasses() {
-    return Iterables.concat(Collections.singleton(target), getToMergeClasses());
+  MergeGroup getGroup() {
+    return group;
   }
 
   void buildClassIdentifierMap() {
-    classIdentifiers.put(target.type, 0);
-    for (DexProgramClass toMerge : toMergeGroup) {
-      classIdentifiers.put(toMerge.type, classIdentifiers.size());
-    }
+    classIdentifiers.put(group.getTarget().getType(), 0);
+    group.forEachSource(clazz -> classIdentifiers.put(clazz.getType(), classIdentifiers.size()));
   }
 
   void mergeDirectMethods(SyntheticArgumentClass syntheticArgumentClass) {
-    mergeDirectMethods(target);
-    toMergeGroup.forEach(this::mergeDirectMethods);
-
+    mergeDirectMethods(group.getTarget());
+    group.forEachSource(this::mergeDirectMethods);
     mergeConstructors(syntheticArgumentClass);
   }
 
@@ -118,7 +103,8 @@
           assert !definition.isClassInitializer();
 
           if (!definition.isInstanceInitializer()) {
-            DexMethod newMethod = method.getReference().withHolder(target.type, dexItemFactory);
+            DexMethod newMethod =
+                method.getReference().withHolder(group.getTarget().getType(), dexItemFactory);
             if (!classMethodsBuilder.isFresh(newMethod)) {
               newMethod = renameDirectMethod(method);
             }
@@ -144,7 +130,7 @@
         method.getDefinition().method.name.toSourceString(),
         method.getHolderType(),
         method.getDefinition().proto(),
-        target.type,
+        group.getTarget().getType(),
         classMethodsBuilder::isFresh);
   }
 
@@ -164,38 +150,47 @@
         merger ->
             merger.merge(
                 classMethodsBuilder, lensBuilder, fieldAccessChangesBuilder, classIdentifiers));
-    toMergeGroup.forEach(clazz -> clazz.getMethodCollection().clearVirtualMethods());
+    group.forEachSource(clazz -> clazz.getMethodCollection().clearVirtualMethods());
   }
 
   void appendClassIdField() {
     DexEncodedField encodedField =
         new DexEncodedField(
-            classIdField,
+            group.getClassIdField(),
             FieldAccessFlags.fromSharedAccessFlags(
                 Constants.ACC_PUBLIC + Constants.ACC_FINAL + Constants.ACC_SYNTHETIC),
             FieldTypeSignature.noSignature(),
             DexAnnotationSet.empty(),
             null);
-    target.appendInstanceField(encodedField);
+    group.getTarget().appendInstanceField(encodedField);
   }
 
   void mergeStaticFields() {
-    toMergeGroup.forEach(classStaticFieldsMerger::addFields);
-    classStaticFieldsMerger.merge(target);
-    toMergeGroup.forEach(clazz -> clazz.setStaticFields(null));
+    group.forEachSource(classStaticFieldsMerger::addFields);
+    classStaticFieldsMerger.merge(group.getTarget());
+    group.forEachSource(clazz -> clazz.setStaticFields(null));
   }
 
   void fixAccessFlags() {
-    if (Iterables.any(toMergeGroup, not(DexProgramClass::isAbstract))) {
-      target.getAccessFlags().demoteFromAbstract();
+    if (Iterables.any(group.getSources(), not(DexProgramClass::isAbstract))) {
+      group.getTarget().getAccessFlags().demoteFromAbstract();
     }
-    if (Iterables.any(toMergeGroup, not(DexProgramClass::isFinal))) {
-      target.getAccessFlags().demoteFromFinal();
+    if (Iterables.any(group.getSources(), not(DexProgramClass::isFinal))) {
+      group.getTarget().getAccessFlags().demoteFromFinal();
+    }
+  }
+
+  private void mergeInterfaces() {
+    DexTypeList previousInterfaces = group.getTarget().getInterfaces();
+    Set<DexType> interfaces = Sets.newLinkedHashSet(previousInterfaces);
+    group.forEachSource(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
+    if (interfaces.size() > previousInterfaces.size()) {
+      group.getTarget().setInterfaces(new DexTypeList(interfaces));
     }
   }
 
   void mergeInstanceFields() {
-    toMergeGroup.forEach(
+    group.forEachSource(
         clazz -> {
           classInstanceFieldsMerger.addFields(clazz);
           clazz.setInstanceFields(null);
@@ -207,65 +202,71 @@
     fixAccessFlags();
     appendClassIdField();
 
+    mergeInterfaces();
+
     mergeVirtualMethods();
     mergeDirectMethods(syntheticArgumentClass);
-    classMethodsBuilder.setClassMethods(target);
+    classMethodsBuilder.setClassMethods(group.getTarget());
 
     mergeStaticFields();
     mergeInstanceFields();
 
-    mergedClassesBuilder.addMergeGroup(target, toMergeGroup);
+    mergedClassesBuilder.addMergeGroup(group);
   }
 
   public static class Builder {
     private final AppView<AppInfoWithLiveness> appView;
-    private final DexProgramClass target;
-    private final List<DexProgramClass> toMergeGroup = new ArrayList<>();
+    private final MergeGroup group;
     private final Map<DexProto, ConstructorMerger.Builder> constructorMergerBuilders =
         new LinkedHashMap<>();
+    private final List<ConstructorMerger.Builder> unmergedConstructorBuilders = new ArrayList<>();
     private final Map<Wrapper<DexMethod>, VirtualMethodMerger.Builder> virtualMethodMergerBuilders =
         new LinkedHashMap<>();
 
-    public Builder(AppView<AppInfoWithLiveness> appView, DexProgramClass target) {
+    public Builder(AppView<AppInfoWithLiveness> appView, MergeGroup group) {
       this.appView = appView;
-      this.target = target;
+      this.group = group;
+    }
+
+    private Builder setup() {
+      DexItemFactory dexItemFactory = appView.dexItemFactory();
+      DexProgramClass target = group.iterator().next();
+      // TODO(b/165498187): ensure the name for the field is fresh
+      group.setClassIdField(
+          dexItemFactory.createField(
+              target.getType(), dexItemFactory.intType, CLASS_ID_FIELD_NAME));
+      group.setTarget(target);
       setupForMethodMerging(target);
-    }
-
-    public Builder mergeClass(DexProgramClass toMerge) {
-      setupForMethodMerging(toMerge);
-      toMergeGroup.add(toMerge);
+      group.forEachSource(this::setupForMethodMerging);
       return this;
     }
 
-    public Builder addClassesToMerge(List<DexProgramClass> toMerge) {
-      toMerge.forEach(this::mergeClass);
-      return this;
-    }
-
-    void setupForMethodMerging(DexProgramClass toMerge) {
+    private void setupForMethodMerging(DexProgramClass toMerge) {
       toMerge.forEachProgramDirectMethod(
           method -> {
             DexEncodedMethod definition = method.getDefinition();
             assert !definition.isClassInitializer();
-
             if (definition.isInstanceInitializer()) {
               addConstructor(method);
             }
           });
-
       toMerge.forEachProgramVirtualMethod(this::addVirtualMethod);
     }
 
-    void addConstructor(ProgramMethod method) {
+    private void addConstructor(ProgramMethod method) {
       assert method.getDefinition().isInstanceInitializer();
-      constructorMergerBuilders
-          .computeIfAbsent(
-              method.getDefinition().getProto(), ignore -> new ConstructorMerger.Builder(appView))
-          .add(method.getDefinition());
+      if (appView.options().enableHorizontalClassMergingConstructorMerging) {
+        constructorMergerBuilders
+            .computeIfAbsent(
+                method.getDefinition().getProto(), ignore -> new ConstructorMerger.Builder(appView))
+            .add(method.getDefinition());
+      } else {
+        unmergedConstructorBuilders.add(
+            new ConstructorMerger.Builder(appView).add(method.getDefinition()));
+      }
     }
 
-    void addVirtualMethod(ProgramMethod method) {
+    private void addVirtualMethod(ProgramMethod method) {
       assert method.getDefinition().isNonPrivateVirtualMethod();
       virtualMethodMergerBuilders
           .computeIfAbsent(
@@ -274,21 +275,21 @@
           .add(method);
     }
 
+    private Collection<ConstructorMerger.Builder> getConstructorMergerBuilders() {
+      return appView.options().enableHorizontalClassMergingConstructorMerging
+          ? constructorMergerBuilders.values()
+          : unmergedConstructorBuilders;
+    }
+
     public ClassMerger build(
-        AppView<AppInfoWithLiveness> appView,
         HorizontallyMergedClasses.Builder mergedClassesBuilder,
         HorizontalClassMergerGraphLens.Builder lensBuilder,
         FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder) {
-      DexItemFactory dexItemFactory = appView.dexItemFactory();
-      // TODO(b/165498187): ensure the name for the field is fresh
-      DexField classIdField =
-          dexItemFactory.createField(target.type, dexItemFactory.intType, CLASS_ID_FIELD_NAME);
-
+      setup();
       List<VirtualMethodMerger> virtualMethodMergers =
           new ArrayList<>(virtualMethodMergerBuilders.size());
       for (VirtualMethodMerger.Builder builder : virtualMethodMergerBuilders.values()) {
-        virtualMethodMergers.add(
-            builder.build(appView, target, classIdField, toMergeGroup.size() + 1));
+        virtualMethodMergers.add(builder.build(appView, group));
       }
       // Try and merge the functions with the most arguments first, to avoid using synthetic
       // arguments if possible.
@@ -296,8 +297,8 @@
 
       List<ConstructorMerger> constructorMergers =
           new ArrayList<>(constructorMergerBuilders.size());
-      for (ConstructorMerger.Builder builder : constructorMergerBuilders.values()) {
-        constructorMergers.addAll(builder.build(appView, target, classIdField));
+      for (ConstructorMerger.Builder builder : getConstructorMergerBuilders()) {
+        constructorMergers.addAll(builder.build(appView, group));
       }
 
       // Try and merge the functions with the most arguments first, to avoid using synthetic
@@ -310,9 +311,7 @@
           lensBuilder,
           mergedClassesBuilder,
           fieldAccessChangesBuilder,
-          target,
-          toMergeGroup,
-          classIdField,
+          group,
           virtualMethodMergers,
           constructorMergers);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMethodsBuilder.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMethodsBuilder.java
index 2516cad..13c01eb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMethodsBuilder.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMethodsBuilder.java
@@ -34,9 +34,9 @@
   }
 
   public void setClassMethods(DexProgramClass clazz) {
-    assert virtualMethods.stream().allMatch(method -> method.holder() == clazz.type);
+    assert virtualMethods.stream().allMatch(method -> method.getHolderType() == clazz.type);
     assert virtualMethods.stream().allMatch(method -> method.belongsToVirtualPool());
-    assert directMethods.stream().allMatch(method -> method.holder() == clazz.type);
+    assert directMethods.stream().allMatch(method -> method.getHolderType() == clazz.type);
     assert directMethods.stream().allMatch(method -> method.belongsToDirectPool());
     clazz.setVirtualMethods(virtualMethods);
     clazz.setDirectMethods(directMethods);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
index e287090..7bc85d1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
@@ -15,21 +15,20 @@
 
 public class ClassStaticFieldsMerger {
   private final Builder lensBuilder;
-  private final DexProgramClass target;
+  private final MergeGroup group;
   private final Map<DexField, DexEncodedField> targetFields = new LinkedHashMap<>();
   private final DexItemFactory dexItemFactory;
-  private final AppView<?> appView;
 
   public ClassStaticFieldsMerger(
-      AppView<?> appView,
-      HorizontalClassMergerGraphLens.Builder lensBuilder,
-      DexProgramClass target) {
-    this.appView = appView;
+      AppView<?> appView, HorizontalClassMergerGraphLens.Builder lensBuilder, MergeGroup group) {
     this.lensBuilder = lensBuilder;
 
-    this.target = target;
+    this.group = group;
     // Add mappings for all target fields.
-    target.staticFields().forEach(field -> targetFields.put(field.getReference(), field));
+    group
+        .getTarget()
+        .staticFields()
+        .forEach(field -> targetFields.put(field.getReference(), field));
 
     this.dexItemFactory = appView.dexItemFactory();
   }
@@ -40,10 +39,11 @@
 
   private void addField(DexEncodedField field) {
     DexField oldFieldReference = field.getReference();
-    DexField templateReference = field.getReference().withHolder(target.type, dexItemFactory);
+    DexField templateReference =
+        field.getReference().withHolder(group.getTarget().getType(), dexItemFactory);
     DexField newFieldReference =
         dexItemFactory.createFreshFieldNameWithHolderSuffix(
-            templateReference, field.holder(), this::isFresh);
+            templateReference, field.getHolderType(), this::isFresh);
 
     field = field.toTypeSubstitutedField(newFieldReference);
     targetFields.put(newFieldReference, field);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
index 2e2020d..d2cd444 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -9,10 +9,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
@@ -35,24 +33,19 @@
 
 public class ConstructorMerger {
   private final AppView<?> appView;
-  private final DexProgramClass target;
+  private final MergeGroup group;
   private final Collection<DexEncodedMethod> constructors;
   private final DexItemFactory dexItemFactory;
-  private final DexField classIdField;
 
   ConstructorMerger(
-      AppView<?> appView,
-      DexProgramClass target,
-      Collection<DexEncodedMethod> constructors,
-      DexField classIdField) {
+      AppView<?> appView, MergeGroup group, Collection<DexEncodedMethod> constructors) {
     this.appView = appView;
-    this.target = target;
+    this.group = group;
     this.constructors = constructors;
-    this.classIdField = classIdField;
 
     // Constructors should not be empty and all constructors should have the same prototype.
     assert !constructors.isEmpty();
-    assert constructors.stream().map(constructor -> constructor.proto()).distinct().count() == 1;
+    assert constructors.stream().map(DexEncodedMethod::proto).distinct().count() == 1;
 
     this.dexItemFactory = appView.dexItemFactory();
   }
@@ -103,12 +96,10 @@
       return this;
     }
 
-    public List<ConstructorMerger> build(
-        AppView<?> appView, DexProgramClass target, DexField classIdField) {
+    public List<ConstructorMerger> build(AppView<?> appView, MergeGroup group) {
       assert constructorGroups.stream().noneMatch(List::isEmpty);
       return ListUtils.map(
-          constructorGroups,
-          constructors -> new ConstructorMerger(appView, target, constructors, classIdField));
+          constructorGroups, constructors -> new ConstructorMerger(appView, group, constructors));
     }
   }
 
@@ -121,17 +112,17 @@
     DexMethod method =
         dexItemFactory.createFreshMethodName(
             "constructor",
-            constructor.holder(),
+            constructor.getHolderType(),
             constructor.proto(),
-            target.type,
+            group.getTarget().getType(),
             classMethodsBuilder::isFresh);
 
     DexEncodedMethod encodedMethod = constructor.toTypeSubstitutedMethod(method);
     encodedMethod.getMutableOptimizationInfo().markForceInline();
-    encodedMethod.accessFlags.unsetConstructor();
-    encodedMethod.accessFlags.unsetPublic();
-    encodedMethod.accessFlags.unsetProtected();
-    encodedMethod.accessFlags.setPrivate();
+    encodedMethod.getAccessFlags().unsetConstructor();
+    encodedMethod.getAccessFlags().unsetPublic();
+    encodedMethod.getAccessFlags().unsetProtected();
+    encodedMethod.getAccessFlags().setPrivate();
     classMethodsBuilder.addDirectMethod(encodedMethod);
 
     return method;
@@ -169,8 +160,8 @@
     DexMethod methodReferenceTemplate = generateReferenceMethodTemplate();
     DexMethod newConstructorReference =
         dexItemFactory.createInstanceInitializerWithFreshProto(
-            methodReferenceTemplate.withHolder(target.type, dexItemFactory),
-            syntheticArgumentClass.getArgumentClass(),
+            methodReferenceTemplate.withHolder(group.getTarget().getType(), dexItemFactory),
+            syntheticArgumentClass.getArgumentClasses(),
             classMethodsBuilder::isFresh);
     int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
 
@@ -179,7 +170,7 @@
         new ConstructorEntryPointSynthesizedCode(
             typeConstructorClassMap,
             newConstructorReference,
-            classIdField,
+            group.getClassIdField(),
             appView.graphLens().getOriginalMethodSignature(representativeConstructorReference));
     DexEncodedMethod newConstructor =
         new DexEncodedMethod(
@@ -223,6 +214,7 @@
 
     classMethodsBuilder.addDirectMethod(newConstructor);
 
-    fieldAccessChangesBuilder.fieldWrittenByMethod(classIdField, newConstructorReference);
+    fieldAccessChangesBuilder.fieldWrittenByMethod(
+        group.getClassIdField(), newConstructorReference);
   }
 }
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 8dfa67c..0821a4c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -9,27 +9,27 @@
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.ClassesHaveSameInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.DontInlinePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeIntoLessVisible;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.IgnoreSynthetics;
+import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
 import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
+import com.android.tools.r8.horizontalclassmerging.policies.NoIndirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinLambdas;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
-import com.android.tools.r8.horizontalclassmerging.policies.NoRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoServiceLoaders;
 import com.android.tools.r8.horizontalclassmerging.policies.NoStaticClassInitializer;
-import com.android.tools.r8.horizontalclassmerging.policies.NotEntryPoint;
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NotVerticallyMergedIntoSubtype;
-import com.android.tools.r8.horizontalclassmerging.policies.PreventChangingVisibility;
+import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
 import com.android.tools.r8.horizontalclassmerging.policies.PreventMergeIntoMainDex;
 import com.android.tools.r8.horizontalclassmerging.policies.PreventMethodImplementation;
 import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
@@ -61,10 +61,10 @@
       DirectMappedDexApplication.Builder appBuilder,
       MainDexTracingResult mainDexTracingResult,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
-    List<DexProgramClass> initialGroup = appView.appInfo().classesWithDeterministicOrder();
+    MergeGroup initialGroup = new MergeGroup(appView.appInfo().classesWithDeterministicOrder());
 
     // Run the policies on all program classes to produce a final grouping.
-    Collection<List<DexProgramClass>> groups =
+    Collection<MergeGroup> groups =
         new SimplePolicyExecutor()
             .run(
                 Collections.singletonList(initialGroup),
@@ -88,7 +88,8 @@
         initializeClassMergers(
             mergedClassesBuilder, lensBuilder, fieldAccessChangesBuilder, groups);
     Iterable<DexProgramClass> allMergeClasses =
-        Iterables.concat(Iterables.transform(classMergers, ClassMerger::getClasses));
+        Iterables.concat(
+            Iterables.transform(classMergers, classMerger -> classMerger.getGroup().getClasses()));
 
     // Merge the classes.
     SyntheticArgumentClass syntheticArgumentClass =
@@ -109,7 +110,6 @@
         new NotMatchedByNoHorizontalClassMerging(appView),
         new SameFields(),
         new NoInterfaces(),
-        new ClassesHaveSameInterfaces(),
         new NoAnnotations(),
         new NoEnums(appView),
         new CheckAbstractClasses(appView),
@@ -123,18 +123,19 @@
         new NoKotlinLambdas(appView),
         new NoServiceLoaders(appView),
         new NotVerticallyMergedIntoSubtype(appView),
-        new NoRuntimeTypeChecks(runtimeTypeCheckInfo),
-        new NotEntryPoint(appView.dexItemFactory()),
+        new NoDirectRuntimeTypeChecks(runtimeTypeCheckInfo),
+        new NoIndirectRuntimeTypeChecks(appView, runtimeTypeCheckInfo),
         new PreventMethodImplementation(appView),
         new DontInlinePolicy(appView, mainDexTracingResult),
         new PreventMergeIntoMainDex(appView, mainDexTracingResult),
         new AllInstantiatedOrUninstantiated(appView),
         new SameParentClass(),
         new SameNestHost(),
-        new PreventChangingVisibility(),
+        new PreserveMethodCharacteristics(),
         new SameFeatureSplit(appView),
         new RespectPackageBoundaries(appView),
         new DontMergeSynchronizedClasses(appView),
+        new LimitGroups(appView),
         // TODO(b/166577694): no policies should be run after this policy, as it would
         // potentially break tests
         new DontMergeIntoLessVisible()
@@ -150,20 +151,15 @@
       HorizontallyMergedClasses.Builder mergedClassesBuilder,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
-      Collection<List<DexProgramClass>> groups) {
+      Collection<MergeGroup> groups) {
     List<ClassMerger> classMergers = new ArrayList<>();
 
     // TODO(b/166577694): Replace Collection<DexProgramClass> with MergeGroup
-    for (List<DexProgramClass> group : groups) {
+    for (MergeGroup group : groups) {
       assert !group.isEmpty();
-
-      DexProgramClass target = group.stream().findFirst().get();
-      group.remove(target);
-
       ClassMerger merger =
-          new ClassMerger.Builder(appView, target)
-              .addClassesToMerge(group)
-              .build(appView, mergedClassesBuilder, lensBuilder, fieldAccessChangesBuilder);
+          new ClassMerger.Builder(appView, group)
+              .build(mergedClassesBuilder, lensBuilder, fieldAccessChangesBuilder);
       classMergers.add(merger);
     }
 
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 cc0528f..16f853e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -10,8 +10,10 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -22,7 +24,7 @@
 import java.util.function.Function;
 
 public class HorizontalClassMergerGraphLens extends NestedGraphLens {
-  private final AppView<?> appView;
+
   private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters;
   private final Map<DexMethod, DexMethod> extraOriginalMethodSignatures;
   private final HorizontallyMergedClasses mergedClasses;
@@ -35,7 +37,7 @@
       Map<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
       BiMap<DexField, DexField> originalFieldSignatures,
-      BiMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
       Map<DexMethod, DexMethod> extraOriginalMethodSignatures,
       Map<DexField, DexField> extraOriginalFieldSignatures,
       GraphLens previousLens) {
@@ -47,19 +49,31 @@
         originalMethodSignatures,
         previousLens,
         appView.dexItemFactory());
-    this.appView = appView;
     this.methodExtraParameters = methodExtraParameters;
     this.extraOriginalFieldSignatures = extraOriginalFieldSignatures;
     this.extraOriginalMethodSignatures = extraOriginalMethodSignatures;
     this.mergedClasses = mergedClasses;
   }
 
+  private boolean isSynthesizedByHorizontalClassMerging(DexMethod method) {
+    return methodExtraParameters.containsKey(method);
+  }
+
   @Override
   protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
     return IterableUtils.prependSingleton(previous, mergedClasses.getSourcesFor(previous));
   }
 
   @Override
+  public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(DexMethod method) {
+    if (isSynthesizedByHorizontalClassMerging(method)) {
+      // If we are processing the call site, the arguments should be removed.
+      return RewrittenPrototypeDescription.none();
+    }
+    return super.lookupPrototypeChangesForMethodDefinition(method);
+  }
+
+  @Override
   public DexMethod getOriginalMethodSignature(DexMethod method) {
     DexMethod originalConstructor = extraOriginalMethodSignatures.get(method);
     if (originalConstructor == null) {
@@ -127,7 +141,7 @@
           methodExtraParameters,
           fieldMap.getForwardMap(),
           methodMap.getForwardMap(),
-          inverseFieldMap.getBiMap(),
+          inverseFieldMap.getBiMap().getForwardBacking(),
           inverseMethodMap.getBiMap(),
           inverseMethodMap.getExtraMap(),
           inverseFieldMap.getExtraMap(),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index 5432ef1..76ab41a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -5,12 +5,10 @@
 package com.android.tools.r8.horizontalclassmerging;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
-import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -84,10 +82,8 @@
       return new HorizontallyMergedClasses(mergedClasses);
     }
 
-    public void addMergeGroup(DexProgramClass target, Collection<DexProgramClass> toMergeGroup) {
-      for (DexProgramClass clazz : toMergeGroup) {
-        mergedClasses.put(clazz.type, target.type);
-      }
+    public void addMergeGroup(MergeGroup group) {
+      group.forEachSource(clazz -> mergedClasses.put(clazz.getType(), group.getTarget().getType()));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java
index 8da9b0f..973162f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java
@@ -4,20 +4,20 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import com.google.common.collect.BiMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import java.util.Map;
 
 /** The inverse of a {@link ManyToOneMap} used for generating graph lens maps. */
 public class ManyToOneInverseMap<K, V> {
-  private final BiMap<V, K> biMap;
+  private final BidirectionalOneToOneHashMap<V, K> biMap;
   private final Map<V, K> extraMap;
 
-  ManyToOneInverseMap(BiMap<V, K> biMap, Map<V, K> extraMap) {
+  ManyToOneInverseMap(BidirectionalOneToOneHashMap<V, K> biMap, Map<V, K> extraMap) {
     this.biMap = biMap;
     this.extraMap = extraMap;
   }
 
-  public BiMap<V, K> getBiMap() {
+  public BidirectionalOneToOneHashMap<V, K> getBiMap() {
     return biMap;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java
index 675d24c..5204021 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import java.util.HashMap;
@@ -49,7 +50,7 @@
   }
 
   public ManyToOneInverseMap<K, V> inverse(Function<Set<K>, K> pickRepresentative) {
-    BiMap<V, K> biMap = HashBiMap.create();
+    BidirectionalOneToOneHashMap<V, K> biMap = new BidirectionalOneToOneHashMap<>();
     Map<V, K> extraMap = new HashMap<>();
     for (Entry<V, Set<K>> entry : inverseMap.entrySet()) {
       K representative = representativeMap.get(entry.getKey());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
new file mode 100644
index 0000000..02e4684
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -0,0 +1,128 @@
+/*
+ *  // Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+ *  // for details. All rights reserved. Use of this source code is governed by a
+ *  // BSD-style license that can be found in the LICENSE file.
+ */
+
+package com.android.tools.r8.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.IteratorUtils;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class MergeGroup implements Iterable<DexProgramClass> {
+
+  public static class Metadata {}
+
+  private final LinkedList<DexProgramClass> classes;
+
+  private DexField classIdField;
+  private DexProgramClass target = null;
+  private Metadata metadata = null;
+
+  public MergeGroup() {
+    this.classes = new LinkedList<>();
+  }
+
+  public MergeGroup(Collection<DexProgramClass> classes) {
+    this();
+    addAll(classes);
+  }
+
+  public void applyMetadataFrom(MergeGroup group) {
+    if (metadata == null) {
+      metadata = group.metadata;
+    }
+  }
+
+  public void add(DexProgramClass clazz) {
+    classes.add(clazz);
+  }
+
+  public void addAll(Collection<DexProgramClass> classes) {
+    this.classes.addAll(classes);
+  }
+
+  public void addFirst(DexProgramClass clazz) {
+    classes.addFirst(clazz);
+  }
+
+  public void forEachSource(Consumer<DexProgramClass> consumer) {
+    assert hasTarget();
+    for (DexProgramClass clazz : classes) {
+      if (clazz != target) {
+        consumer.accept(clazz);
+      }
+    }
+  }
+
+  public LinkedList<DexProgramClass> getClasses() {
+    return classes;
+  }
+
+  public boolean hasClassIdField() {
+    return classIdField != null;
+  }
+
+  public DexField getClassIdField() {
+    assert hasClassIdField();
+    return classIdField;
+  }
+
+  public void setClassIdField(DexField classIdField) {
+    this.classIdField = classIdField;
+  }
+
+  public Iterable<DexProgramClass> getSources() {
+    assert hasTarget();
+    return Iterables.filter(classes, clazz -> clazz != target);
+  }
+
+  public boolean hasTarget() {
+    return target != null;
+  }
+
+  public DexProgramClass getTarget() {
+    return target;
+  }
+
+  public void setTarget(DexProgramClass target) {
+    assert classes.contains(target);
+    this.target = target;
+  }
+
+  public boolean isTrivial() {
+    return size() < 2;
+  }
+
+  public boolean isEmpty() {
+    return classes.isEmpty();
+  }
+
+  @Override
+  public Iterator<DexProgramClass> iterator() {
+    return classes.iterator();
+  }
+
+  public int size() {
+    return classes.size();
+  }
+
+  public DexProgramClass removeFirst(Predicate<DexProgramClass> predicate) {
+    return IteratorUtils.removeFirst(iterator(), predicate);
+  }
+
+  public boolean removeIf(Predicate<DexProgramClass> predicate) {
+    return classes.removeIf(predicate);
+  }
+
+  public DexProgramClass removeLast() {
+    return classes.removeLast();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
index eb5d4d0..b21f233 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -4,25 +4,17 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import com.android.tools.r8.graph.DexProgramClass;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.List;
 
 public abstract class MultiClassPolicy extends Policy {
 
-  // TODO(b/165577835): Move to a virtual method on MergeGroup.
-  protected boolean isTrivial(Collection<DexProgramClass> group) {
-    return group.size() < 2;
-  }
-
   /**
    * Remove all groups containing no or only a single class, as there is no point in merging these.
    */
-  protected Collection<List<DexProgramClass>> removeTrivialGroups(
-      Collection<List<DexProgramClass>> groups) {
+  protected Collection<MergeGroup> removeTrivialGroups(Collection<MergeGroup> groups) {
     assert !(groups instanceof ArrayList);
-    groups.removeIf(this::isTrivial);
+    groups.removeIf(MergeGroup::isTrivial);
     return groups;
   }
 
@@ -34,5 +26,5 @@
    *     merged. If the policy detects no issues then `group` will be returned unchanged. If classes
    *     cannot be merged with any other classes they are returned as singleton lists.
    */
-  public abstract Collection<List<DexProgramClass>> apply(List<DexProgramClass> group);
+  public abstract Collection<MergeGroup> apply(MergeGroup group);
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
index bf460d9..3cea5125 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
@@ -7,16 +7,15 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import java.util.Collection;
 import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 
 public abstract class MultiClassSameReferencePolicy<T> extends MultiClassPolicy {
+
   @Override
-  public final Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    Map<T, List<DexProgramClass>> groups = new LinkedHashMap<>();
+  public final Collection<MergeGroup> apply(MergeGroup group) {
+    Map<T, MergeGroup> groups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
-      groups.computeIfAbsent(getMergeKey(clazz), ignore -> new LinkedList<>()).add(clazz);
+      groups.computeIfAbsent(getMergeKey(clazz), ignore -> new MergeGroup()).add(clazz);
     }
     removeTrivialGroups(groups.values());
     return groups.values();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
index a5f58d2..b94adf8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -4,9 +4,7 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import com.android.tools.r8.graph.DexProgramClass;
 import java.util.Collection;
-import java.util.List;
 
 public abstract class PolicyExecutor {
 
@@ -15,6 +13,6 @@
    * policies registered to this policy executor on the class groups yielding a new collection of
    * class groups.
    */
-  public abstract Collection<List<DexProgramClass>> run(
-      Collection<List<DexProgramClass>> classes, Collection<Policy> policies);
+  public abstract Collection<MergeGroup> run(
+      Collection<MergeGroup> classes, Collection<Policy> policies);
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
index 92130de..b2fdb3d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -4,12 +4,10 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.IterableUtils;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * This is a simple policy executor that ensures regular sequential execution of policies. It should
@@ -19,11 +17,11 @@
 public class SimplePolicyExecutor extends PolicyExecutor {
 
   // TODO(b/165506334): if performing mutable operation ensure that linked lists are used
-  private LinkedList<List<DexProgramClass>> applySingleClassPolicy(
-      SingleClassPolicy policy, LinkedList<List<DexProgramClass>> groups) {
-    Iterator<List<DexProgramClass>> i = groups.iterator();
+  private LinkedList<MergeGroup> applySingleClassPolicy(
+      SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
+    Iterator<MergeGroup> i = groups.iterator();
     while (i.hasNext()) {
-      Collection<DexProgramClass> group = i.next();
+      MergeGroup group = i.next();
       int previousNumberOfClasses = group.size();
       group.removeIf(clazz -> !policy.canMerge(clazz));
       policy.numberOfRemovedClasses += previousNumberOfClasses - group.size();
@@ -34,30 +32,29 @@
     return groups;
   }
 
-  private LinkedList<List<DexProgramClass>> applyMultiClassPolicy(
-      MultiClassPolicy policy, LinkedList<List<DexProgramClass>> groups) {
+  private LinkedList<MergeGroup> applyMultiClassPolicy(
+      MultiClassPolicy policy, LinkedList<MergeGroup> groups) {
     // For each group apply the multi class policy and add all the new groups together.
-    return groups.stream()
-        .flatMap(
-            group -> {
-              int previousNumberOfClasses = group.size();
-              Collection<List<DexProgramClass>> newGroups = policy.apply(group);
-              policy.numberOfRemovedClasses += previousNumberOfClasses;
-              for (List<DexProgramClass> newGroup : newGroups) {
-                policy.numberOfRemovedClasses -= newGroup.size();
-              }
-              return newGroups.stream();
-            })
-        .collect(Collectors.toCollection(LinkedList::new));
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          int previousNumberOfClasses = group.size();
+          Collection<MergeGroup> policyGroups = policy.apply(group);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          policy.numberOfRemovedClasses +=
+              previousNumberOfClasses - IterableUtils.sumInt(policyGroups, MergeGroup::size);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
   }
 
   @Override
-  public Collection<List<DexProgramClass>> run(
-      Collection<List<DexProgramClass>> inputGroups, Collection<Policy> policies) {
-    LinkedList<List<DexProgramClass>> linkedGroups;
+  public Collection<MergeGroup> run(
+      Collection<MergeGroup> inputGroups, Collection<Policy> policies) {
+    LinkedList<MergeGroup> linkedGroups;
 
     if (inputGroups instanceof LinkedList) {
-      linkedGroups = (LinkedList<List<DexProgramClass>>) inputGroups;
+      linkedGroups = (LinkedList<MergeGroup>) inputGroups;
     } else {
       linkedGroups = new LinkedList<>(inputGroups);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
index 28ec4a9..b6e1f53 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
@@ -17,7 +17,9 @@
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 
 /**
  * Lets assume we are merging a class A that looks like:
@@ -39,29 +41,32 @@
 public class SyntheticArgumentClass {
   public static final String SYNTHETIC_CLASS_SUFFIX = "$r8$HorizontalClassMergingArgument";
 
-  private final DexType syntheticClassType;
+  private final List<DexType> syntheticClassTypes;
 
-  private SyntheticArgumentClass(DexType syntheticClassType) {
-    this.syntheticClassType = syntheticClassType;
+  private SyntheticArgumentClass(List<DexType> syntheticClassTypes) {
+    this.syntheticClassTypes = syntheticClassTypes;
   }
 
-  public DexType getArgumentClass() {
-    return syntheticClassType;
+  public List<DexType> getArgumentClasses() {
+    return syntheticClassTypes;
   }
 
   public static class Builder {
 
-    public SyntheticArgumentClass build(
+    private DexType synthesizeClass(
         AppView<AppInfoWithLiveness> appView,
         DirectMappedDexApplication.Builder appBuilder,
-        Iterable<DexProgramClass> mergeClasses) {
+        DexProgramClass context,
+        boolean requiresMainDex,
+        int index) {
 
-      // Find a fresh name in an existing package.
-      DexProgramClass context = mergeClasses.iterator().next();
       DexType syntheticClassType =
-          context.type.addSuffix(SYNTHETIC_CLASS_SUFFIX, appView.dexItemFactory());
-
-      boolean requiresMainDex = appView.appInfo().getMainDexClasses().containsAnyOf(mergeClasses);
+          appView
+              .dexItemFactory()
+              .createFreshTypeName(
+                  context.type.addSuffix(SYNTHETIC_CLASS_SUFFIX, appView.dexItemFactory()),
+                  type -> appView.appInfo().definitionForWithoutExistenceAssert(type) == null,
+                  index);
 
       DexProgramClass clazz =
           new DexProgramClass(
@@ -88,7 +93,26 @@
       appBuilder.addSynthesizedClass(clazz);
       appView.appInfo().addSynthesizedClass(clazz, requiresMainDex);
 
-      return new SyntheticArgumentClass(clazz.type);
+      return clazz.type;
+    }
+
+    public SyntheticArgumentClass build(
+        AppView<AppInfoWithLiveness> appView,
+        DirectMappedDexApplication.Builder appBuilder,
+        Iterable<DexProgramClass> mergeClasses) {
+
+      // Find a fresh name in an existing package.
+      DexProgramClass context = mergeClasses.iterator().next();
+
+      boolean requiresMainDex = appView.appInfo().getMainDexClasses().containsAnyOf(mergeClasses);
+
+      List<DexType> syntheticArgumentTypes = new ArrayList<>();
+      for (int i = 0; i < appView.options().horizontalClassMergingSyntheticArgumentCount; i++) {
+        syntheticArgumentTypes.add(
+            synthesizeClass(appView, appBuilder, context, requiresMainDex, i));
+      }
+
+      return new SyntheticArgumentClass(syntheticArgumentTypes);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
index 49399bf..da61076 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -256,7 +256,7 @@
         newMethodReference =
             dexItemFactory.createInstanceInitializerWithFreshProto(
                 newMethodReference,
-                syntheticArgumentClass.getArgumentClass(),
+                syntheticArgumentClass.getArgumentClasses(),
                 tryMethod -> !newMethods.contains(tryMethod.getSignature()));
         int extraNulls = newMethodReference.getArity() - originalMethodReference.getArity();
         lensBuilder.addExtraParameters(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 258b1c7..fe3a898 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -32,22 +31,19 @@
 import java.util.List;
 
 public class VirtualMethodMerger {
-  private final DexProgramClass target;
   private final DexItemFactory dexItemFactory;
+  private final MergeGroup group;
   private final List<ProgramMethod> methods;
-  private final DexField classIdField;
   private final AppView<AppInfoWithLiveness> appView;
   private final DexMethod superMethod;
 
   public VirtualMethodMerger(
       AppView<AppInfoWithLiveness> appView,
-      DexProgramClass target,
+      MergeGroup group,
       List<ProgramMethod> methods,
-      DexField classIdField,
       DexMethod superMethod) {
     this.dexItemFactory = appView.dexItemFactory();
-    this.target = target;
-    this.classIdField = classIdField;
+    this.group = group;
     this.methods = methods;
     this.appView = appView;
     this.superMethod = superMethod;
@@ -84,15 +80,11 @@
       return resolutionResult.getResolvedMethod().getReference();
     }
 
-    public VirtualMethodMerger build(
-        AppView<AppInfoWithLiveness> appView,
-        DexProgramClass target,
-        DexField classIdField,
-        int mergeGroupSize) {
+    public VirtualMethodMerger build(AppView<AppInfoWithLiveness> appView, MergeGroup group) {
       // If not all the classes are in the merge group, find the fallback super method to call.
-      DexMethod superMethod = methods.size() < mergeGroupSize ? superMethod(appView, target) : null;
-
-      return new VirtualMethodMerger(appView, target, methods, classIdField, superMethod);
+      DexMethod superMethod =
+          methods.size() < group.size() ? superMethod(appView, group.getTarget()) : null;
+      return new VirtualMethodMerger(appView, group, methods, superMethod);
     }
   }
 
@@ -111,11 +103,11 @@
             oldMethodReference.name.toSourceString(),
             oldMethod.getHolderType(),
             oldMethodReference.proto,
-            target.type,
+            group.getTarget().getType(),
             classMethodsBuilder::isFresh);
 
     DexEncodedMethod encodedMethod = oldMethod.getDefinition().toTypeSubstitutedMethod(method);
-    MethodAccessFlags flags = encodedMethod.accessFlags;
+    MethodAccessFlags flags = encodedMethod.getAccessFlags();
     flags.unsetProtected();
     flags.unsetPublic();
     flags.setPrivate();
@@ -138,7 +130,7 @@
   }
 
   private DexMethod getNewMethodReference() {
-    return ListUtils.first(methods).getReference().withHolder(target, dexItemFactory);
+    return ListUtils.first(methods).getReference().withHolder(group.getTarget(), dexItemFactory);
   }
 
   /**
@@ -190,7 +182,7 @@
       }
     }
 
-    if (representative.getHolderType() == target.getType()) {
+    if (representative.getHolder() == group.getTarget()) {
       classMethodsBuilder.addVirtualMethod(representative.getDefinition());
     } else {
       // If the method is not in the target type, move it.
@@ -262,7 +254,7 @@
     AbstractSynthesizedCode synthesizedCode =
         new VirtualMethodEntryPointSynthesizedCode(
             classIdToMethodMap,
-            classIdField,
+            group.getClassIdField(),
             superMethod,
             newMethodReference,
             bridgeMethodReference);
@@ -292,6 +284,6 @@
 
     classMethodsBuilder.addVirtualMethod(newMethod);
 
-    fieldAccessChangesBuilder.fieldReadByMethod(classIdField, newMethod.method);
+    fieldAccessChangesBuilder.fieldReadByMethod(group.getClassIdField(), newMethod.method);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java
index 4839801..6e5c711 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java
@@ -6,15 +6,17 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses.AbstractClassification;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.Lists;
-import java.util.Collection;
-import java.util.LinkedList;
-import java.util.List;
 
-public class CheckAbstractClasses extends MultiClassPolicy {
+public class CheckAbstractClasses extends MultiClassSameReferencePolicy<AbstractClassification> {
+
+  enum AbstractClassification {
+    ABSTRACT,
+    NOT_ABSTRACT
+  }
 
   private final InternalOptions options;
 
@@ -23,28 +25,16 @@
   }
 
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    if (options.canUseAbstractMethodOnNonAbstractClass()) {
-      // We can just make the target class non-abstract if one of the classes in the group
-      // is non-abstract.
-      return Lists.<List<DexProgramClass>>newArrayList(group);
-    }
-    List<DexProgramClass> abstractClasses = new LinkedList<>();
-    List<DexProgramClass> nonAbstractClasses = new LinkedList<>();
-    for (DexProgramClass clazz : group) {
-      if (clazz.isAbstract()) {
-        abstractClasses.add(clazz);
-      } else {
-        nonAbstractClasses.add(clazz);
-      }
-    }
-    List<List<DexProgramClass>> newGroups = new LinkedList<>();
-    if (abstractClasses.size() > 1) {
-      newGroups.add(abstractClasses);
-    }
-    if (nonAbstractClasses.size() > 1) {
-      newGroups.add(nonAbstractClasses);
-    }
-    return newGroups;
+  public boolean shouldSkipPolicy() {
+    // We can just make the target class non-abstract if one of the classes in the group
+    // is non-abstract.
+    return options.canUseAbstractMethodOnNonAbstractClass();
+  }
+
+  @Override
+  public AbstractClassification getMergeKey(DexProgramClass clazz) {
+    return clazz.isAbstract()
+        ? AbstractClassification.ABSTRACT
+        : AbstractClassification.NOT_ABSTRACT;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ClassesHaveSameInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ClassesHaveSameInterfaces.java
deleted file mode 100644
index 060e1a6..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/ClassesHaveSameInterfaces.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
-
-public class ClassesHaveSameInterfaces extends MultiClassSameReferencePolicy<DexTypeList> {
-
-  @Override
-  public DexTypeList getMergeKey(DexProgramClass clazz) {
-    return clazz.interfaces.getSorted();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeIntoLessVisible.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeIntoLessVisible.java
index 4e9b69e..954a238 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeIntoLessVisible.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeIntoLessVisible.java
@@ -4,30 +4,21 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
 
 public class DontMergeIntoLessVisible extends MultiClassPolicy {
-  @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    Iterator<DexProgramClass> iterator = group.iterator();
-    while (iterator.hasNext()) {
-      DexProgramClass clazz = iterator.next();
-      if (clazz.isPublic()) {
-        iterator.remove();
-        List<DexProgramClass> newGroup = new LinkedList<>();
-        newGroup.add(clazz);
-        newGroup.addAll(group);
-        group = newGroup;
-        break;
-      }
-    }
 
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    DexProgramClass clazz = group.removeFirst(DexClass::isPublic);
+    if (clazz != null) {
+      group.addFirst(clazz);
+    }
     return Collections.singletonList(group);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java
index d8bebf9..4372869 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java
@@ -6,13 +6,13 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedList;
-import java.util.List;
 
 public class DontMergeSynchronizedClasses extends MultiClassPolicy {
   private final AppView<AppInfoWithLiveness> appView;
@@ -26,14 +26,14 @@
   }
 
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+  public Collection<MergeGroup> apply(MergeGroup group) {
     // Gather all synchronized classes.
-    Collection<List<DexProgramClass>> synchronizedGroups = new LinkedList<>();
+    Collection<MergeGroup> synchronizedGroups = new LinkedList<>();
     group.removeIf(
         clazz -> {
           boolean synchronizationClass = isSynchronizationClass(clazz);
           if (synchronizationClass) {
-            List<DexProgramClass> synchronizedGroup = new LinkedList<>();
+            MergeGroup synchronizedGroup = new MergeGroup();
             synchronizedGroup.add(clazz);
             synchronizedGroups.add(synchronizedGroup);
           }
@@ -44,7 +44,7 @@
       return Collections.singletonList(group);
     }
 
-    Iterator<List<DexProgramClass>> synchronizedGroupIterator = synchronizedGroups.iterator();
+    Iterator<MergeGroup> synchronizedGroupIterator = synchronizedGroups.iterator();
     for (DexProgramClass clazz : group) {
       if (!synchronizedGroupIterator.hasNext()) {
         synchronizedGroupIterator = synchronizedGroups.iterator();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
new file mode 100644
index 0000000..f2800aa
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+
+public class LimitGroups extends MultiClassPolicy {
+
+  private final int maxGroupSize;
+
+  public LimitGroups(AppView<AppInfoWithLiveness> appView) {
+    maxGroupSize = appView.options().horizontalClassMergingMaxGroupSize;
+    assert maxGroupSize >= 2;
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    if (group.size() <= maxGroupSize) {
+      return Collections.singletonList(group);
+    }
+
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    MergeGroup newGroup = createNewGroup(newGroups);
+    for (DexProgramClass clazz : group) {
+      if (newGroup.size() == maxGroupSize) {
+        newGroup = createNewGroup(newGroups);
+      }
+      newGroup.add(clazz);
+    }
+    if (newGroup.size() == 1) {
+      if (maxGroupSize == 2) {
+        MergeGroup removedGroup = newGroups.removeLast();
+        assert removedGroup == newGroup;
+      } else {
+        newGroup.add(newGroups.getFirst().removeLast());
+      }
+    }
+    return newGroups;
+  }
+
+  private MergeGroup createNewGroup(LinkedList<MergeGroup> newGroups) {
+    MergeGroup newGroup = new MergeGroup();
+    newGroups.add(newGroup);
+    return newGroup;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoRuntimeTypeChecks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
similarity index 75%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoRuntimeTypeChecks.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
index 010fee8..b0f3766 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoRuntimeTypeChecks.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
@@ -8,16 +8,15 @@
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
 
-public class NoRuntimeTypeChecks extends SingleClassPolicy {
+public class NoDirectRuntimeTypeChecks extends SingleClassPolicy {
   private final RuntimeTypeCheckInfo runtimeTypeCheckInfo;
 
-  public NoRuntimeTypeChecks(RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+  public NoDirectRuntimeTypeChecks(RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     this.runtimeTypeCheckInfo = runtimeTypeCheckInfo;
   }
 
   @Override
   public boolean canMerge(DexProgramClass clazz) {
-    // We currently assume we only merge classes that implement the same set of interfaces.
     return !runtimeTypeCheckInfo.isRuntimeCheckType(clazz);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java
new file mode 100644
index 0000000..7446787
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMap;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+
+public class NoIndirectRuntimeTypeChecks extends MultiClassSameReferencePolicy<DexTypeList> {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final RuntimeTypeCheckInfo runtimeTypeCheckInfo;
+
+  private final Reference2BooleanMap<DexType> cache = new Reference2BooleanOpenHashMap<>();
+
+  public NoIndirectRuntimeTypeChecks(
+      AppView<AppInfoWithLiveness> appView, RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+    this.appView = appView;
+    this.runtimeTypeCheckInfo = runtimeTypeCheckInfo;
+  }
+
+  @Override
+  public DexTypeList getMergeKey(DexProgramClass clazz) {
+    // Require that classes that implement an interface that has a runtime type check (directly or
+    // indirectly on a parent interface) are only merged with classes that implement the same
+    // interfaces.
+    return clazz
+        .getInterfaces()
+        .keepIf(this::computeInterfaceHasDirectOrIndirectRuntimeTypeCheck)
+        .getSorted();
+  }
+
+  private boolean computeInterfaceHasDirectOrIndirectRuntimeTypeCheck(DexType type) {
+    if (cache.containsKey(type)) {
+      return cache.getBoolean(type);
+    }
+    DexClass clazz = appView.definitionFor(type);
+    if (clazz == null || !clazz.isInterface()) {
+      cache.put(type, true);
+      return true;
+    }
+    if (!clazz.isProgramClass()) {
+      // Conservatively return true because there could be a type check in the library.
+      cache.put(type, true);
+      return true;
+    }
+    if (runtimeTypeCheckInfo.isRuntimeCheckType(clazz.asProgramClass())) {
+      cache.put(type, true);
+      return true;
+    }
+    for (DexType parentType : clazz.getInterfaces()) {
+      if (computeInterfaceHasDirectOrIndirectRuntimeTypeCheck(parentType)) {
+        cache.put(type, true);
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
deleted file mode 100644
index 245b391..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-
-public class NoOverlappingConstructors extends MultiClassPolicy {
-
-  public void removeNonConflicting(Map<DexProto, Set<DexProgramClass>> overlappingConstructors) {
-    Iterator<DexProto> i = overlappingConstructors.keySet().iterator();
-    while (i.hasNext()) {
-      DexProto proto = i.next();
-      if (overlappingConstructors.get(proto).size() == 1) {
-        i.remove();
-      }
-    }
-  }
-
-  private Set<DexProgramClass> sortedClassSet(Collection<DexProgramClass> classes) {
-    Set<DexProgramClass> set =
-        new TreeSet<DexProgramClass>(Comparator.comparing(DexProgramClass::getType));
-    set.addAll(classes);
-    return set;
-  }
-
-  @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    Map<DexProto, Set<DexProgramClass>> overlappingConstructors = new IdentityHashMap<>();
-
-    for (DexProgramClass clazz : group) {
-      clazz.forEachProgramDirectMethod(
-          directMethod -> {
-            DexEncodedMethod method = directMethod.getDefinition();
-            if (method.isInstanceInitializer()) {
-              overlappingConstructors
-                  .computeIfAbsent(method.getProto(), ignore -> new HashSet<DexProgramClass>())
-                  .add(clazz);
-            }
-          });
-    }
-
-    removeNonConflicting(overlappingConstructors);
-
-    // This is probably related to the graph colouring problem so probably won't be efficient in
-    // worst cases. We assume there won't be that many overlapping constructors so this should be
-    // reasonable. Constructor merging should also make this obsolete.
-    Collection<Set<DexProgramClass>> groups = new LinkedList<>();
-    groups.add(sortedClassSet(group));
-
-    for (Set<DexProgramClass> overlappingClasses : overlappingConstructors.values()) {
-      Collection<Set<DexProgramClass>> newGroups = new LinkedList<>();
-
-      // For every set of classes that cannot be in the same group, generate a new group with the
-      // same constructor and with all remaining constructors.
-
-      for (Set<DexProgramClass> existingGroup : groups) {
-        Set<DexProgramClass> actuallyOverlapping = new HashSet<>(overlappingClasses);
-        actuallyOverlapping.retainAll(existingGroup);
-
-        if (actuallyOverlapping.size() <= 1) {
-          newGroups.add(existingGroup);
-        } else {
-          Set<DexProgramClass> notOverlapping = new HashSet<>(existingGroup);
-          notOverlapping.removeAll(overlappingClasses);
-          for (DexProgramClass overlappingClass : actuallyOverlapping) {
-            Set<DexProgramClass> newGroup = sortedClassSet(notOverlapping);
-            newGroup.add(overlappingClass);
-            newGroups.add(newGroup);
-          }
-        }
-      }
-
-      groups = newGroups;
-    }
-
-    // Ensure each class is only in a single group and remove singleton and empty groups.
-    Set<DexProgramClass> assignedClasses = new HashSet<>();
-
-    Iterator<Set<DexProgramClass>> i = groups.iterator();
-    while (i.hasNext()) {
-      Set<DexProgramClass> newGroup = i.next();
-      newGroup.removeAll(assignedClasses);
-      if (newGroup.size() <= 1) {
-        i.remove();
-      } else {
-        assignedClasses.addAll(newGroup);
-      }
-    }
-
-    // Map to collection
-    Collection<List<DexProgramClass>> newGroups = new ArrayList<>();
-    for (Set<DexProgramClass> newGroup : groups) {
-      newGroups.add(new ArrayList<>(newGroup));
-    }
-    return newGroups;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java
deleted file mode 100644
index 4bc65e5..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NotEntryPoint extends SingleClassPolicy {
-  private final DexString main;
-
-  public NotEntryPoint(DexItemFactory factory) {
-    main = factory.createString("main");
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    // TODO(b/165000217): Account for keep rules instead.
-    for (DexEncodedMethod method : program.directMethods()) {
-      if (method.method.name.equals(main)) {
-        return false;
-      }
-    }
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
new file mode 100644
index 0000000..adcde5c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Policy that enforces that methods are only merged if they have the same visibility and library
+ * method override information.
+ */
+public class PreserveMethodCharacteristics extends MultiClassPolicy {
+
+  static class MethodCharacteristics {
+
+    private final int visibilityOrdinal;
+
+    private MethodCharacteristics(DexEncodedMethod method) {
+      this.visibilityOrdinal = method.getAccessFlags().getVisibilityOrdinal();
+    }
+
+    @Override
+    public int hashCode() {
+      return visibilityOrdinal;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (getClass() != obj.getClass()) {
+        return false;
+      }
+      MethodCharacteristics characteristics = (MethodCharacteristics) obj;
+      return visibilityOrdinal == characteristics.visibilityOrdinal;
+    }
+  }
+
+  public PreserveMethodCharacteristics() {}
+
+  public static class TargetGroup {
+
+    private final MergeGroup group = new MergeGroup();
+    private final Map<DexMethodSignature, MethodCharacteristics> methodMap = new HashMap<>();
+
+    public MergeGroup getGroup() {
+      return group;
+    }
+
+    public boolean tryAdd(DexProgramClass clazz) {
+      Map<DexMethodSignature, MethodCharacteristics> newMethods = new HashMap<>();
+      for (DexEncodedMethod method : clazz.methods()) {
+        DexMethodSignature signature = method.getSignature();
+        MethodCharacteristics existingCharacteristics = methodMap.get(signature);
+        MethodCharacteristics methodCharacteristics = new MethodCharacteristics(method);
+        if (existingCharacteristics == null) {
+          newMethods.put(signature, methodCharacteristics);
+          continue;
+        }
+        if (!methodCharacteristics.equals(existingCharacteristics)) {
+          return false;
+        }
+      }
+      methodMap.putAll(newMethods);
+      group.add(clazz);
+      return true;
+    }
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    List<TargetGroup> groups = new ArrayList<>();
+
+    for (DexProgramClass clazz : group) {
+      boolean added = Iterables.any(groups, targetGroup -> targetGroup.tryAdd(clazz));
+      if (!added) {
+        TargetGroup newGroup = new TargetGroup();
+        added = newGroup.tryAdd(clazz);
+        assert added;
+        groups.add(newGroup);
+      }
+    }
+
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    for (TargetGroup newGroup : groups) {
+      if (!newGroup.getGroup().isTrivial()) {
+        newGroups.add(newGroup.getGroup());
+      }
+    }
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java
index 8b35a5c..cd58e10 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventChangingVisibility.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
@@ -15,7 +16,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -23,10 +23,10 @@
   public PreventChangingVisibility() {}
 
   public static class TargetGroup {
-    private final List<DexProgramClass> group = new LinkedList<>();
+    private final MergeGroup group = new MergeGroup();
     private final Map<Wrapper<DexMethod>, MethodAccessFlags> methodMap = new HashMap<>();
 
-    public List<DexProgramClass> getGroup() {
+    public MergeGroup getGroup() {
       return group;
     }
 
@@ -53,7 +53,7 @@
   }
 
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+  public Collection<MergeGroup> apply(MergeGroup group) {
     List<TargetGroup> groups = new ArrayList<>();
 
     for (DexProgramClass clazz : group) {
@@ -66,13 +66,12 @@
       }
     }
 
-    Collection<List<DexProgramClass>> newGroups = new ArrayList<>();
+    Collection<MergeGroup> newGroups = new ArrayList<>();
     for (TargetGroup newGroup : groups) {
-      if (!isTrivial(newGroup.getGroup())) {
+      if (!newGroup.getGroup().isTrivial()) {
         newGroups.add(newGroup.getGroup());
       }
     }
-
     return newGroups;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java
index b8083e1..dcbd291 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.horizontalclassmerging.SubtypingForrestForClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -19,8 +20,6 @@
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -144,13 +143,13 @@
   }
 
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+  public Collection<MergeGroup> apply(MergeGroup group) {
     DexMethodSignatureSet signatures = DexMethodSignatureSet.createLinked();
     for (DexProgramClass clazz : group) {
       signatures.addAllMethods(clazz.methods());
     }
 
-    Map<DispatchSignature, List<DexProgramClass>> newGroups = new LinkedHashMap<>();
+    Map<DispatchSignature, MergeGroup> newGroups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       DexMethodSignatureSet clazzReserved = computeReservedSignaturesForClass(clazz);
       DispatchSignature dispatchSignature = new DispatchSignature();
@@ -166,7 +165,7 @@
         }
         dispatchSignature.addSignature(signature, category);
       }
-      newGroups.computeIfAbsent(dispatchSignature, ignore -> new LinkedList<>()).add(clazz);
+      newGroups.computeIfAbsent(dispatchSignature, ignore -> new MergeGroup()).add(clazz);
     }
     return removeTrivialGroups(newGroups.values());
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
index bd59a0e..914fc57 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/RespectPackageBoundaries.java
@@ -7,16 +7,14 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
 import com.android.tools.r8.utils.TraversalContinuation;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 
 public class RespectPackageBoundaries extends MultiClassPolicy {
@@ -58,30 +56,28 @@
 
   /** Sort unrestricted classes into restricted classes if they are in the same package. */
   void tryFindRestrictedPackage(
-      LinkedList<DexProgramClass> unrestrictedClasses,
-      Map<String, List<DexProgramClass>> restrictedClasses) {
-    Iterator<DexProgramClass> i = unrestrictedClasses.iterator();
-    while (i.hasNext()) {
-      DexProgramClass clazz = i.next();
-      Collection<DexProgramClass> restrictedPackage =
-          restrictedClasses.get(clazz.type.getPackageDescriptor());
-      if (restrictedPackage != null) {
-        restrictedPackage.add(clazz);
-        i.remove();
-      }
-    }
+      MergeGroup unrestrictedClasses, Map<String, MergeGroup> restrictedClasses) {
+    unrestrictedClasses.removeIf(
+        clazz -> {
+          MergeGroup restrictedPackage = restrictedClasses.get(clazz.type.getPackageDescriptor());
+          if (restrictedPackage != null) {
+            restrictedPackage.add(clazz);
+            return true;
+          }
+          return false;
+        });
   }
 
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    Map<String, List<DexProgramClass>> restrictedClasses = new LinkedHashMap<>();
-    LinkedList<DexProgramClass> unrestrictedClasses = new LinkedList<>();
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    Map<String, MergeGroup> restrictedClasses = new LinkedHashMap<>();
+    MergeGroup unrestrictedClasses = new MergeGroup();
 
     // Sort all restricted classes into packages.
     for (DexProgramClass clazz : group) {
       if (shouldRestrictMergingAcrossPackageBoundary(clazz)) {
         restrictedClasses
-            .computeIfAbsent(clazz.type.getPackageDescriptor(), ignore -> new ArrayList<>())
+            .computeIfAbsent(clazz.getType().getPackageDescriptor(), ignore -> new MergeGroup())
             .add(clazz);
       } else {
         unrestrictedClasses.add(clazz);
@@ -93,7 +89,7 @@
 
     // TODO(b/166577694): Add the unrestricted classes to restricted groups, but ensure they aren't
     // the merge target.
-    Collection<List<DexProgramClass>> groups = new ArrayList<>(restrictedClasses.size() + 1);
+    Collection<MergeGroup> groups = new ArrayList<>(restrictedClasses.size() + 1);
     if (unrestrictedClasses.size() > 1) {
       groups.add(unrestrictedClasses);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ClassInitializationAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ClassInitializationAnalysis.java
index 43fe1ac..dbaf662 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/ClassInitializationAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/ClassInitializationAnalysis.java
@@ -337,7 +337,7 @@
       if (!resolutionResult.isSingleResolution()) {
         return false;
       }
-      DexType holder = resolutionResult.getSingleTarget().holder();
+      DexType holder = resolutionResult.getSingleTarget().getHolderType();
       return appView.isSubtype(holder, type).isTrue();
     }
 
@@ -398,7 +398,7 @@
       if (!resolutionResult.isSingleResolution()) {
         return false;
       }
-      DexType holder = resolutionResult.getSingleTarget().holder();
+      DexType holder = resolutionResult.getSingleTarget().getHolderType();
       return appView.isSubtype(holder, type).isTrue();
     }
 
@@ -435,7 +435,7 @@
       if (!resolutionResult.isSingleResolution()) {
         return false;
       }
-      DexType holder = resolutionResult.getSingleTarget().holder();
+      DexType holder = resolutionResult.getSingleTarget().getHolderType();
       return appView.isSubtype(holder, type).isTrue();
     }
 
@@ -511,11 +511,11 @@
         enqueue(clazz.type, visited, worklist);
       } else if (definition.isDexEncodedField()) {
         DexEncodedField field = definition.asDexEncodedField();
-        enqueue(field.holder(), visited, worklist);
+        enqueue(field.getHolderType(), visited, worklist);
       } else if (definition.isDexEncodedMethod()) {
         assert instruction.isInvokeMethod();
         DexEncodedMethod method = definition.asDexEncodedMethod();
-        enqueue(method.holder(), visited, worklist);
+        enqueue(method.getHolderType(), visited, worklist);
         enqueueInitializedClassesOnNormalExit(method, instruction.inValues(), visited, worklist);
       } else {
         assert false;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/InitializedClassesOnNormalExitAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/InitializedClassesOnNormalExitAnalysis.java
index 01ea16b..eb26afa 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/InitializedClassesOnNormalExitAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/InitializedClassesOnNormalExitAnalysis.java
@@ -117,8 +117,8 @@
       DexEncodedField field =
           appView.appInfo().resolveField(instruction.getField()).getResolvedField();
       if (field != null) {
-        if (field.holder().isClassType()) {
-          markInitializedOnNormalExit(field.holder());
+        if (field.getHolderType().isClassType()) {
+          markInitializedOnNormalExit(field.getHolderType());
         } else {
           assert false : "Expected holder of field type to be a class type";
         }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
index 6e46731..2cf0214 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
@@ -21,7 +21,7 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewArrayFilledData;
@@ -255,7 +255,7 @@
     }
 
     // Find the single constructor invocation.
-    InvokeMethod constructorInvoke =
+    InvokeDirect constructorInvoke =
         newInstance.getUniqueConstructorInvoke(appView.dexItemFactory());
     if (constructorInvoke == null || constructorInvoke.getInvokedMethod().holder != clazz.type) {
       // Didn't find a (valid) constructor invocation, give up.
@@ -269,7 +269,7 @@
     }
 
     InstanceInitializerInfo initializerInfo =
-        constructor.getOptimizationInfo().getInstanceInitializerInfo();
+        constructor.getOptimizationInfo().getInstanceInitializerInfo(constructorInvoke);
 
     List<DexEncodedField> fields = clazz.getDirectAndIndirectInstanceFields(appView);
     if (!fields.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
index d0fd119..d0a86ef 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/FieldAssignmentTracker.java
@@ -162,7 +162,7 @@
         singleTarget
             .getDefinition()
             .getOptimizationInfo()
-            .getInstanceInitializerInfo()
+            .getInstanceInitializerInfo(invoke)
             .fieldInitializationInfos();
 
     // Synchronize on the lattice element (abstractInstanceFieldValuesForClass) in case we process
@@ -232,7 +232,7 @@
         InstanceFieldInitializationInfo fieldInitializationInfo =
             method
                 .getOptimizationInfo()
-                .getInstanceInitializerInfo()
+                .getContextInsensitiveInstanceInitializerInfo()
                 .fieldInitializationInfos()
                 .get(field);
         if (fieldInitializationInfo.isSingleValue()) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
index d42d5d1..443ac30 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
@@ -120,7 +120,7 @@
 
   @Override
   boolean isSubjectToOptimization(DexEncodedField field) {
-    return !field.isStatic() && field.holder() == context.getHolderType();
+    return !field.isStatic() && field.getHolderType() == context.getHolderType();
   }
 
   @Override
@@ -147,7 +147,7 @@
         singleTarget
             .getDefinition()
             .getOptimizationInfo()
-            .getInstanceInitializerInfo()
+            .getInstanceInitializerInfo(invoke)
             .fieldInitializationInfos();
     for (DexEncodedField field : singleTarget.getHolder().instanceFields()) {
       assert isSubjectToOptimization(field);
@@ -169,7 +169,7 @@
         parentConstructor
             .getDefinition()
             .getOptimizationInfo()
-            .getInstanceInitializerInfo()
+            .getInstanceInitializerInfo(parentConstructorCall)
             .fieldInitializationInfos();
     infos.forEach(
         appView,
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index c75b845..9be6c55 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -91,7 +91,7 @@
   @Override
   boolean isSubjectToOptimization(DexEncodedField field) {
     return field.isStatic()
-        && field.holder() == context.getHolderType()
+        && field.getHolderType() == context.getHolderType()
         && appView.appInfo().isFieldOnlyWrittenInMethod(field, context.getDefinition());
   }
 
@@ -348,7 +348,7 @@
         singleTarget
             .getDefinition()
             .getOptimizationInfo()
-            .getInstanceInitializerInfo()
+            .getInstanceInitializerInfo(uniqueConstructorInvoke)
             .fieldInitializationInfos();
     if (initializationInfos.isEmpty()) {
       return ObjectState.empty();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 04f9a85..e9b454a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -123,7 +123,7 @@
    */
   public void rewriteCode(DexEncodedMethod method, IRCode code) {
     if (method.isClassInitializer()
-        && classesWithRemovedExtensionFields.contains(method.holder())
+        && classesWithRemovedExtensionFields.contains(method.getHolderType())
         && code.metadata().mayHaveStaticPut()) {
       rewriteClassInitializer(code);
     }
@@ -176,12 +176,18 @@
                 clazz.forEachProgramMethodMatching(
                     definition ->
                         references.isFindLiteExtensionByNumberMethod(definition.getReference()),
-                    consumer::accept),
+                    consumer),
             lambda -> {
               assert false;
             });
   }
 
+  public void handleFailedOrUnknownFieldResolution(DexField fieldReference, ProgramMethod context) {
+    if (references.isFindLiteExtensionByNumberMethod(context)) {
+      removedExtensionFields.add(fieldReference);
+    }
+  }
+
   public boolean isDeadProtoExtensionField(DexField fieldReference) {
     AppInfoWithLiveness appInfo = appView.appInfo();
     FieldResolutionResult resolutionResult = appInfo.resolveField(fieldReference);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
index c44747f..d84295c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/schema/ProtoEnqueuerExtension.java
@@ -267,7 +267,8 @@
             continue;
           }
 
-          DexProgramClass holder = asProgramClassOrNull(appView.definitionFor(field.holder()));
+          DexProgramClass holder =
+              asProgramClassOrNull(appView.definitionFor(field.getHolderType()));
           if (holder == null) {
             assert false;
             continue;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
index 5d771a5..c6fdbab 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
@@ -73,7 +73,7 @@
         DexEncodedField field =
             appView.appInfo().resolveField(staticPut.getField()).getResolvedField();
         if (field == null
-            || field.holder() != context.getHolderType()
+            || field.getHolderType() != context.getHolderType()
             || instruction.instructionInstanceCanThrow(appView, context)) {
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
index 3c9bd86..8de1c91 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
@@ -95,7 +95,7 @@
     if (!encodedField.isPublic()) {
       return false;
     }
-    DexClass holder = appView.definitionFor(encodedField.holder());
+    DexClass holder = appView.definitionFor(encodedField.getHolderType());
     if (holder == null) {
       assert false;
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index c61ca0a..b4ff341 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -202,7 +202,7 @@
         return singleTarget
             .getDefinition()
             .getOptimizationInfo()
-            .getInstanceInitializerInfo()
+            .getInstanceInitializerInfo(this)
             .readSet();
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 8928a4d..fdaf8f0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -255,7 +255,9 @@
     DexEncodedMethod singleTargetDefinition = singleTarget.getDefinition();
     MethodOptimizationInfo optimizationInfo = singleTargetDefinition.getOptimizationInfo();
     if (singleTargetDefinition.isInstanceInitializer()) {
-      InstanceInitializerInfo initializerInfo = optimizationInfo.getInstanceInitializerInfo();
+      assert isInvokeDirect();
+      InstanceInitializerInfo initializerInfo =
+          optimizationInfo.getInstanceInitializerInfo(asInvokeDirect());
       if (!initializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
         return !isInvokeDirect();
       }
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 0dd2895..889089f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
 import java.util.ListIterator;
 import java.util.Set;
 
@@ -21,6 +22,7 @@
 
   private BasicBlock currentBlock;
   private InstructionListIterator currentBlockIterator;
+  private Set<BasicBlock> seenBlocks = Sets.newIdentityHashSet();
 
   public LinearFlowInstructionListIterator(IRCode code, BasicBlock block) {
     this(code, block, 0);
@@ -32,12 +34,17 @@
     this.currentBlockIterator = block.listIterator(code, index);
     // If index is pointing after the last instruction, and it is a goto with a linear edge,
     // we have to move the pointer. This is achieved by calling previous and next.
+    seenBlocks.add(block);
     if (index > 0) {
       this.previous();
       this.next();
     }
   }
 
+  public boolean hasVisitedBlock(BasicBlock basicBlock) {
+    return seenBlocks.contains(basicBlock);
+  }
+
   @Override
   public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
     currentBlockIterator.replaceCurrentInstruction(newInstruction, affectedValues);
@@ -146,6 +153,7 @@
       target = candidate;
     }
     currentBlock = target;
+    seenBlocks.add(target);
     currentBlockIterator = currentBlock.listIterator(code);
     return currentBlockIterator.next();
   }
@@ -183,6 +191,7 @@
       return currentBlockIterator.previous();
     }
     currentBlock = target;
+    seenBlocks.add(target);
     currentBlockIterator = currentBlock.listIterator(code, currentBlock.getInstructions().size());
     // Iterate over the jump.
     currentBlockIterator.previous();
diff --git a/src/main/java/com/android/tools/r8/ir/code/ValueType.java b/src/main/java/com/android/tools/r8/ir/code/ValueType.java
index 729cdcf..8b06849 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ValueType.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ValueType.java
@@ -9,14 +9,26 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
 
-public enum ValueType {
+public enum ValueType implements StructuralItem<ValueType> {
   OBJECT,
   INT,
   FLOAT,
   LONG,
   DOUBLE;
 
+  @Override
+  public ValueType self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<ValueType> getStructuralAccept() {
+    return spec -> spec.withInt(Enum::ordinal);
+  }
+
   public boolean isObject() {
     return this == OBJECT;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
index 6756693..967fee0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
@@ -262,7 +262,8 @@
         return;
       }
 
-      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(encodedField.holder()));
+      DexProgramClass clazz =
+          asProgramClassOrNull(appView.definitionFor(encodedField.getHolderType()));
       if (clazz == null) {
         return;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 7f29860..e0841f7 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -331,7 +331,7 @@
       }
     }
     return new CfCode(
-        method.holder(),
+        method.getHolderType(),
         stackHeightTracker.maxHeight,
         registerAllocator.registersUsed(),
         instructions,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 74191f0..042441e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -531,7 +531,7 @@
     int argumentIndex = 0;
 
     if (!method.isStatic()) {
-      writeCallback.accept(register, method.holder());
+      writeCallback.accept(register, method.getHolderType());
       addThisArgument(register);
       argumentIndex++;
       register++;
@@ -1530,7 +1530,7 @@
       if (invocationMethod.holder == method.getHolderType()) {
         DexEncodedMethod directTarget = method.getHolder().lookupDirectMethod(invocationMethod);
         if (directTarget != null && !directTarget.isStatic()) {
-          assert invocationMethod.holder == directTarget.holder();
+          assert invocationMethod.holder == directTarget.getHolderType();
           type = Type.DIRECT;
         }
       }
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 ec95001..0974715 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
@@ -107,10 +107,12 @@
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -480,7 +482,33 @@
         executor, OptimizationFeedbackIgnore.getInstance());
     DexApplication application = appView.appInfo().app();
     timing.begin("IR conversion");
-    ThreadUtils.processItems(application.classes(), this::convertMethods, executor);
+
+    if (appView.options().desugarSpecificOptions().allowAllDesugaredInput) {
+      // Classes which has already been through library desugaring will not go through IR
+      // processing again.
+      LibraryDesugaredChecker libraryDesugaredChecker = new LibraryDesugaredChecker(appView);
+      Set<DexProgramClass> alreadyLibraryDesugared = Sets.newConcurrentHashSet();
+      ThreadUtils.processItems(
+          application.classes(),
+          clazz -> {
+            if (libraryDesugaredChecker.isClassLibraryDesugared(clazz)) {
+              if (appView.options().desugarSpecificOptions().allowAllDesugaredInput) {
+                alreadyLibraryDesugared.add(clazz);
+              } else {
+                throw new CompilationError(
+                    "Code for "
+                        + clazz.getType().getDescriptor()
+                        + "has already been library desugared.");
+              }
+            } else {
+              convertMethods(clazz);
+            }
+          },
+          executor);
+      appView.setAlreadyLibraryDesugared(alreadyLibraryDesugared);
+    } else {
+      ThreadUtils.processItems(application.classes(), this::convertMethods, executor);
+    }
 
     // Build a new application with jumbo string info,
     Builder<?> builder = application.builder();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 20cc2c6..cf605e2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -223,9 +223,11 @@
                   graphLens.lookupMethod(invokedMethod, method.getReference(), invoke.getType());
               DexMethod actualTarget = lensLookup.getReference();
               Invoke.Type actualInvokeType = lensLookup.getType();
-              if (actualTarget != invokedMethod || invoke.getType() != actualInvokeType) {
-                RewrittenPrototypeDescription prototypeChanges = lensLookup.getPrototypeChanges();
 
+              RewrittenPrototypeDescription prototypeChanges = lensLookup.getPrototypeChanges();
+              if (prototypeChanges.requiresRewritingAtCallSite()
+                  || invoke.getType() != actualInvokeType
+                  || actualTarget != invokedMethod) {
                 List<Value> newInValues;
                 ArgumentInfoCollection argumentInfoCollection =
                     prototypeChanges.getArgumentInfoCollection();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LibraryDesugaredChecker.java b/src/main/java/com/android/tools/r8/ir/conversion/LibraryDesugaredChecker.java
new file mode 100644
index 0000000..7707d9e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LibraryDesugaredChecker.java
@@ -0,0 +1,183 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.conversion;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.UseRegistry;
+
+public class LibraryDesugaredChecker {
+  private final AppView<?> appView;
+  private final DexString jDollarDescriptorPrefix;
+
+  LibraryDesugaredChecker(AppView<?> appView) {
+    this.appView = appView;
+    this.jDollarDescriptorPrefix = appView.dexItemFactory().createString("Lj$/");
+  }
+
+  public boolean isClassLibraryDesugared(DexProgramClass clazz) {
+    IsLibraryDesugaredTracer tracer =
+        new IsLibraryDesugaredTracer(appView, jDollarDescriptorPrefix, clazz);
+    tracer.run();
+    return tracer.isLibraryDesugared();
+  }
+
+  private static class IsLibraryDesugaredTracer extends UseRegistry {
+
+    private final DexString jDollarDescriptorPrefix;
+    private final AppView<?> appView;
+    private final DexProgramClass clazz;
+    private boolean isLibraryDesugared = false;
+
+    public IsLibraryDesugaredTracer(
+        AppView<?> appView, DexString jDollarDescriptorPrefix, DexProgramClass clazz) {
+      super(appView.dexItemFactory());
+      this.jDollarDescriptorPrefix = jDollarDescriptorPrefix;
+      this.appView = appView;
+      this.clazz = clazz;
+    }
+
+    public void run() {
+      registerClass(clazz);
+    }
+
+    public boolean isLibraryDesugared() {
+      return isLibraryDesugared;
+    }
+
+    private void registerClass(DexProgramClass clazz) {
+      if (clazz.superType != null) {
+        registerTypeReference(clazz.superType);
+      }
+      for (DexType implementsType : clazz.interfaces.values) {
+        registerTypeReference(implementsType);
+      }
+      if (isLibraryDesugared) {
+        return;
+      }
+      for (DexEncodedMethod method : clazz.methods()) {
+        registerMethod(new ProgramMethod(clazz, method));
+        if (isLibraryDesugared) {
+          return;
+        }
+      }
+      clazz.forEachField(this::registerField);
+    }
+
+    private void registerType(DexType type) {
+      isLibraryDesugared =
+          isLibraryDesugared || type.descriptor.startsWith(jDollarDescriptorPrefix);
+    }
+
+    private void registerField(DexField field) {
+      registerType(field.getHolderType());
+      registerType(field.getType());
+    }
+
+    private void registerMethod(DexMethod method) {
+      for (DexType type : method.getParameters().values) {
+        registerTypeReference(type);
+      }
+      registerTypeReference(method.getReturnType());
+    }
+
+    private void registerField(DexEncodedField field) {
+      registerField(field.getReference());
+    }
+
+    private void registerMethod(ProgramMethod method) {
+      registerMethod(method.getReference());
+      for (DexAnnotation annotation : method.getDefinition().annotations().annotations) {
+        if (annotation.annotation.type == appView.dexItemFactory().annotationThrows) {
+          DexValueArray dexValues = annotation.annotation.elements[0].value.asDexValueArray();
+          for (DexValue dexValType : dexValues.getValues()) {
+            registerType(dexValType.asDexValueType().value);
+          }
+        }
+      }
+      method.registerCodeReferences(this);
+    }
+
+    @Override
+    public void registerInitClass(DexType type) {
+      registerType(type);
+    }
+
+    @Override
+    public void registerInvokeVirtual(DexMethod method) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInvokeDirect(DexMethod method) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInvokeStatic(DexMethod method) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInvokeInterface(DexMethod method) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInvokeStatic(DexMethod method, boolean itf) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInvokeSuper(DexMethod method) {
+      registerMethod(method);
+    }
+
+    @Override
+    public void registerInstanceFieldRead(DexField field) {
+      registerField(field);
+    }
+
+    @Override
+    public void registerInstanceFieldWrite(DexField field) {
+      registerField(field);
+    }
+
+    @Override
+    public void registerNewInstance(DexType type) {
+      registerType(type);
+    }
+
+    @Override
+    public void registerStaticFieldRead(DexField field) {
+      registerField(field);
+    }
+
+    @Override
+    public void registerStaticFieldWrite(DexField field) {
+      registerField(field);
+    }
+
+    @Override
+    public void registerTypeReference(DexType type) {
+      registerType(type);
+    }
+
+    @Override
+    public void registerInstanceOf(DexType type) {
+      registerType(type);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
index 4379da1..44f5990 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodOptimizationFeedback.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.ParameterUsagesInfo;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.BitSet;
 import java.util.Set;
@@ -60,8 +60,8 @@
 
   void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibilityInfo eligibility);
 
-  void setInstanceInitializerInfo(
-      DexEncodedMethod method, InstanceInitializerInfo instanceInitializerInfo);
+  void setInstanceInitializerInfoCollection(
+      DexEncodedMethod method, InstanceInitializerInfoCollection instanceInitializerInfoCollection);
 
   void setInitializerEnablingJavaVmAssertions(DexEncodedMethod method);
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index 0884ab5..84fd203 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -437,7 +437,7 @@
             iface -> {
               for (int i = 0; i < extraInterfaceSignatures.size(); i++) {
                 if (extraInterfaceSignatures.get(i).type() == iface) {
-                  if (!appView.options().desugarSpecificOptions().allowDesugaredInput) {
+                  if (!appView.options().desugarSpecificOptions().allowAllDesugaredInput) {
                     throw new CompilationError(
                         "Code has already been library desugared. Interface "
                             + iface.getDescriptor()
@@ -476,6 +476,9 @@
       ClassInfo superInfo,
       MethodSignatures signatures,
       Builder<DexEncodedMethod> additionalForwards) {
+    if (clazz.isProgramClass() && appView.isAlreadyLibraryDesugared(clazz.asProgramClass())) {
+      return;
+    }
     for (Wrapper<DexMethod> wrapper : signatures.signatures) {
       resolveForwardForSignature(
           clazz,
@@ -553,7 +556,7 @@
 
   private boolean isRetargetMethod(DexLibraryClass holder, DexEncodedMethod method) {
     assert needsLibraryInfo();
-    assert holder.type == method.holder();
+    assert holder.type == method.getHolderType();
     assert method.isNonPrivateVirtualMethod();
     if (method.isFinal()) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
index 9356caa..cf85276 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -107,7 +107,7 @@
 
   public void desugar(IRCode code) {
 
-    if (wrapperSynthesizor.hasSynthesized(code.method().holder())) {
+    if (wrapperSynthesizor.hasSynthesized(code.method().getHolderType())) {
       return;
     }
 
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 214c17a..e04505d 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
@@ -446,6 +446,9 @@
       }
       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;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
index e2f3271..361ab4b 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
@@ -221,7 +221,7 @@
     DexTypeList interfaces =
         isItf ? new DexTypeList(new DexType[] {wrappingType}) : DexTypeList.empty();
     return classKind.create(
-        wrapperField.holder(),
+        wrapperField.getHolderType(),
         Kind.CF,
         new SynthesizedOrigin("Desugared library API Converter", getClass()),
         ClassAccessFlags.fromSharedAccessFlags(
@@ -258,24 +258,26 @@
     //   return v3;
     Set<DexMethod> finalMethods = Sets.newIdentityHashSet();
     for (DexEncodedMethod dexEncodedMethod : dexMethods) {
-      DexClass holderClass = appView.definitionFor(dexEncodedMethod.holder());
+      DexClass holderClass = appView.definitionFor(dexEncodedMethod.getHolderType());
       boolean isInterface;
       if (holderClass == null) {
         assert appView
             .options()
             .desugaredLibraryConfiguration
             .getEmulateLibraryInterface()
-            .containsValue(dexEncodedMethod.holder());
+            .containsValue(dexEncodedMethod.getHolderType());
         isInterface = true;
       } else {
         isInterface = holderClass.isInterface();
       }
       DexMethod methodToInstall =
           factory.createMethod(
-              wrapperField.holder(), dexEncodedMethod.method.proto, dexEncodedMethod.method.name);
+              wrapperField.getHolderType(),
+              dexEncodedMethod.method.proto,
+              dexEncodedMethod.method.name);
       CfCode cfCode;
       if (dexEncodedMethod.isFinal()) {
-        invalidWrappers.add(wrapperField.holder());
+        invalidWrappers.add(wrapperField.getHolderType());
         finalMethods.add(dexEncodedMethod.method);
         continue;
       } else {
@@ -311,15 +313,15 @@
     //   return v3;
     Set<DexMethod> finalMethods = Sets.newIdentityHashSet();
     for (DexEncodedMethod dexEncodedMethod : dexMethods) {
-      DexClass holderClass = appView.definitionFor(dexEncodedMethod.holder());
+      DexClass holderClass = appView.definitionFor(dexEncodedMethod.getHolderType());
       assert holderClass != null || appView.options().isDesugaredLibraryCompilation();
       boolean isInterface = holderClass == null || holderClass.isInterface();
       DexMethod methodToInstall =
           DesugaredLibraryAPIConverter.methodWithVivifiedTypeInSignature(
-              dexEncodedMethod.method, wrapperField.holder(), appView);
+              dexEncodedMethod.method, wrapperField.getHolderType(), appView);
       CfCode cfCode;
       if (dexEncodedMethod.isFinal()) {
-        invalidWrappers.add(wrapperField.holder());
+        invalidWrappers.add(wrapperField.getHolderType());
         finalMethods.add(dexEncodedMethod.method);
         continue;
       } else {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index ddcb3c7..6c1c172 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -336,7 +336,8 @@
             // WARNING: This may result in incorrect code on older platforms!
             // Retarget call to an appropriate method of companion class.
             DexMethod amendedMethod =
-                amendDefaultMethod(appInfo.definitionFor(encodedMethod.holder()), invokedMethod);
+                amendDefaultMethod(
+                    appInfo.definitionFor(encodedMethod.getHolderType()), invokedMethod);
             instructions.replaceCurrentInstruction(
                 new InvokeStatic(defaultAsMethodOfCompanionClass(amendedMethod),
                     invokeSuper.outValue(), invokeSuper.arguments()));
@@ -348,7 +349,7 @@
                 DexEncodedMethod target =
                     appView.appInfoForDesugaring().lookupSuperTarget(invokedMethod, code.context());
                 if (target != null && target.isDefaultMethod()) {
-                  DexClass holder = appView.definitionFor(target.holder());
+                  DexClass holder = appView.definitionFor(target.getHolderType());
                   if (holder.isLibraryClass() && holder.isInterface()) {
                     instructions.replaceCurrentInstruction(
                         new InvokeStatic(
@@ -368,7 +369,7 @@
                       .appInfoForDesugaring()
                       .lookupSuperTarget(invokeSuper.getInvokedMethod(), code.context());
               if (dexEncodedMethod != null) {
-                DexClass dexClass = appView.definitionFor(dexEncodedMethod.holder());
+                DexClass dexClass = appView.definitionFor(dexEncodedMethod.getHolderType());
                 if (dexClass != null && dexClass.isLibraryClass()) {
                   // Rewriting is required because the super invoke resolves into a missing
                   // method (method is on desugared library). Find out if it needs to be
@@ -508,8 +509,8 @@
       // interfaces.
       return null;
     }
-    if (!singleTarget.isAbstract() && isEmulatedInterface(singleTarget.holder())) {
-      return singleTarget.holder();
+    if (!singleTarget.isAbstract() && isEmulatedInterface(singleTarget.getHolderType())) {
+      return singleTarget.getHolderType();
     }
     return null;
   }
@@ -711,8 +712,8 @@
         }
         emulationMethods.add(
             DexEncodedMethod.toEmulateDispatchLibraryMethod(
-                method.holder(),
-                emulateInterfaceLibraryMethod(method.method, method.holder(), factory),
+                method.getHolderType(),
+                emulateInterfaceLibraryMethod(method.method, method.getHolderType(), factory),
                 companionMethod,
                 libraryMethod,
                 extraDispatchCases,
@@ -1022,6 +1023,9 @@
   // results, since each class implementing an emulated interface should also implement the
   // rewritten one.
   private void transformEmulatedInterfaces(DexProgramClass clazz) {
+    if (appView.isAlreadyLibraryDesugared(clazz)) {
+      return;
+    }
     List<GenericSignature.ClassTypeSignature> newInterfaces = new ArrayList<>();
     GenericSignature.ClassSignature classSignature = clazz.getClassSignature();
     for (int i = 0; i < clazz.interfaces.size(); i++) {
@@ -1074,6 +1078,9 @@
     // First we compute all desugaring *without* introducing forwarding methods.
     for (DexProgramClass clazz : builder.getProgramClasses()) {
       if (shouldProcess(clazz, flavour, false)) {
+        if (appView.isAlreadyLibraryDesugared(clazz)) {
+          continue;
+        }
         processor.processClass(clazz);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriterFixup.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriterFixup.java
index 995f693..ed8077a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriterFixup.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriterFixup.java
@@ -53,7 +53,8 @@
       return null;
     }
     // Map default methods to their companion methods.
-    DexMethod mappedMethod = graphLens.getExtraOriginalMethodSignatures().inverse().get(method);
+    DexMethod mappedMethod =
+        graphLens.getExtraOriginalMethodSignatures().getRepresentativeKey(method);
     if (mappedMethod != null) {
       return mappedMethod;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index 02388be..88e1200 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -48,8 +48,9 @@
 import com.android.tools.r8.ir.synthetic.ForwardMethodBuilder;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -451,7 +452,7 @@
     } else {
       assert code.isCfCode();
       for (CfInstruction insn : code.asCfCode().getInstructions()) {
-        if (insn instanceof CfInvoke && ((CfInvoke) insn).isInvokeSuper(method.holder())) {
+        if (insn instanceof CfInvoke && ((CfInvoke) insn).isInvokeSuper(method.getHolderType())) {
           return false;
         }
       }
@@ -514,15 +515,16 @@
   // are to static companion methods.
   public static class InterfaceProcessorNestedGraphLens extends NestedGraphLens {
 
-    private BiMap<DexMethod, DexMethod> extraOriginalMethodSignatures;
+    private BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod>
+        extraOriginalMethodSignatures;
 
     public InterfaceProcessorNestedGraphLens(
         Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
         Map<DexField, DexField> fieldMap,
         BiMap<DexField, DexField> originalFieldSignatures,
-        BiMap<DexMethod, DexMethod> originalMethodSignatures,
-        BiMap<DexMethod, DexMethod> extraOriginalMethodSignatures,
+        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalOneToOneHashMap<DexMethod, DexMethod> extraOriginalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
       super(
@@ -551,12 +553,13 @@
     }
 
     public void toggleMappingToExtraMethods() {
-      BiMap<DexMethod, DexMethod> tmp = originalMethodSignatures;
+      BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> tmp = originalMethodSignatures;
       this.originalMethodSignatures = extraOriginalMethodSignatures;
       this.extraOriginalMethodSignatures = tmp;
     }
 
-    public BiMap<DexMethod, DexMethod> getExtraOriginalMethodSignatures() {
+    public BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod>
+        getExtraOriginalMethodSignatures() {
       return extraOriginalMethodSignatures;
     }
 
@@ -577,16 +580,14 @@
 
     @Override
     protected DexMethod internalGetPreviousMethodSignature(DexMethod method) {
-      return extraOriginalMethodSignatures.getOrDefault(
-          method, originalMethodSignatures.getOrDefault(method, method));
+      return extraOriginalMethodSignatures.getRepresentativeValueOrDefault(
+          method, originalMethodSignatures.getRepresentativeValueOrDefault(method, method));
     }
 
     @Override
     protected DexMethod internalGetNextMethodSignature(DexMethod method) {
-      return originalMethodSignatures
-          .inverse()
-          .getOrDefault(
-              method, extraOriginalMethodSignatures.inverse().getOrDefault(method, method));
+      return originalMethodSignatures.getRepresentativeKeyOrDefault(
+          method, extraOriginalMethodSignatures.getRepresentativeKeyOrDefault(method, method));
     }
 
     @Override
@@ -600,7 +601,8 @@
 
     public static class Builder extends NestedGraphLens.Builder {
 
-      private final BiMap<DexMethod, DexMethod> extraOriginalMethodSignatures = HashBiMap.create();
+      private final BidirectionalOneToOneHashMap<DexMethod, DexMethod>
+          extraOriginalMethodSignatures = new BidirectionalOneToOneHashMap<>();
 
       public void recordCodeMovedToCompanionClass(DexMethod from, DexMethod to) {
         assert from != to;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
index 45a8824..f661256 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
@@ -96,7 +96,7 @@
     DexEncodedMethod targetMethod = context == null ? null : lookupTargetMethod(appInfo, context);
     if (targetMethod != null) {
       targetAccessFlags = targetMethod.accessFlags.copy();
-      targetHolder = targetMethod.holder();
+      targetHolder = targetMethod.getHolderType();
     } else {
       targetAccessFlags = null;
       targetHolder = null;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index 9688ae8..e4d6f65 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -42,6 +42,7 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.BiMap;
@@ -440,7 +441,7 @@
         Map<DexMethod, DexMethod> methodMap,
         Map<DexField, DexField> fieldMap,
         BiMap<DexField, DexField> originalFieldSignatures,
-        BiMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
       super(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
index c0a1269..6b6e4b0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
@@ -247,7 +247,7 @@
   }
 
   private DexMethod computeFieldBridge(DexEncodedField field, boolean isGet) {
-    DexType holderType = field.holder();
+    DexType holderType = field.getHolderType();
     DexType fieldType = field.field.type;
     int bridgeParameterCount =
         BooleanUtils.intValue(!field.isStatic()) + BooleanUtils.intValue(!isGet);
@@ -268,10 +268,10 @@
   boolean invokeRequiresRewriting(DexEncodedMethod method, DexClassAndMethod context) {
     assert method != null;
     // Rewrite only when targeting other nest members private fields.
-    if (!method.accessFlags.isPrivate() || method.holder() == context.getHolderType()) {
+    if (!method.accessFlags.isPrivate() || method.getHolderType() == context.getHolderType()) {
       return false;
     }
-    DexClass methodHolder = definitionFor(method.holder());
+    DexClass methodHolder = definitionFor(method.getHolderType());
     assert methodHolder != null; // from encodedMethod
     return methodHolder.getNestHost() == context.getHolder().getNestHost();
   }
@@ -279,10 +279,10 @@
   boolean fieldAccessRequiresRewriting(DexEncodedField field, DexClassAndMethod context) {
     assert field != null;
     // Rewrite only when targeting other nest members private fields.
-    if (!field.accessFlags.isPrivate() || field.holder() == context.getHolderType()) {
+    if (!field.accessFlags.isPrivate() || field.getHolderType() == context.getHolderType()) {
       return false;
     }
-    DexClass fieldHolder = definitionFor(field.holder());
+    DexClass fieldHolder = definitionFor(field.getHolderType());
     assert fieldHolder != null; // from encodedField
     return fieldHolder.getNestHost() == context.getHolder().getNestHost();
   }
@@ -304,7 +304,7 @@
   }
 
   DexMethod ensureFieldAccessBridge(DexEncodedField field, boolean isGet) {
-    DexClass holder = definitionFor(field.holder());
+    DexClass holder = definitionFor(field.getHolderType());
     assert holder != null;
     DexMethod bridgeMethod = computeFieldBridge(field, isGet);
     if (holderRequiresBridge(holder)) {
@@ -325,7 +325,7 @@
 
   DexMethod ensureInvokeBridge(DexEncodedMethod method) {
     // We add bridges only when targeting other nest members.
-    DexClass holder = definitionFor(method.holder());
+    DexClass holder = definitionFor(method.getHolderType());
     assert holder != null;
     DexMethod bridgeMethod;
     if (method.isInstanceInitializer()) {
@@ -517,7 +517,7 @@
     }
 
     public DexType getHolder() {
-      return field.holder();
+      return field.getHolderType();
     }
 
     public DexField getField() {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
index 7f99055..8889ec2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
 import com.google.common.collect.ImmutableMap;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -35,7 +36,7 @@
         methodMap,
         ImmutableMap.of(),
         null,
-        null,
+        BidirectionalManyToManyRepresentativeMap.empty(),
         previousLens,
         appView.dexItemFactory());
     // No concurrent maps here, we do not want synchronization overhead.
@@ -110,7 +111,6 @@
   @Override
   public MethodLookupResult internalDescribeLookupMethod(
       MethodLookupResult previous, DexMethod context) {
-    assert originalMethodSignatures == null;
     DexMethod bridge = methodMap.get(previous.getReference());
     if (bridge == null) {
       return previous;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
index 23cd37d..d41fd32 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/TwrCloseResourceRewriter.java
@@ -102,7 +102,7 @@
           new InvokeStatic(twrCloseResourceMethod, null, invoke.inValues()));
 
       // Mark as a class referencing utility class.
-      referencingClasses.add(appInfo.definitionFor(code.method().holder()).asProgramClass());
+      referencingClasses.add(appInfo.definitionFor(code.method().getHolderType()).asProgramClass());
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
index e4b35af..8e78a50 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
@@ -109,7 +109,7 @@
   }
 
   private AssertionTransformation getTransformationForMethod(DexEncodedMethod method) {
-    return getTransformationForType(method.holder());
+    return getTransformationForType(method.getHolderType());
   }
 
   private AssertionTransformation getTransformationForType(DexType type) {
@@ -320,7 +320,7 @@
     if (method.isClassInitializer()) {
       clinit = method;
     } else {
-      DexClass clazz = appView.definitionFor(method.holder());
+      DexClass clazz = appView.definitionFor(method.getHolderType());
       if (clazz == null) {
         return;
       }
@@ -337,7 +337,7 @@
       if (current.isInvokeMethod()) {
         InvokeMethod invoke = current.asInvokeMethod();
         if (invoke.getInvokedMethod() == dexItemFactory.classMethods.desiredAssertionStatus) {
-          if (method.holder() == dexItemFactory.kotlin.assertions.type) {
+          if (method.getHolderType() == dexItemFactory.kotlin.assertions.type) {
             rewriteKotlinAssertionEnable(code, transformation, iterator, invoke);
           } else {
             iterator.replaceCurrentInstruction(code.createIntConstant(0, current.getLocalInfo()));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 8a8d2d4..6e7d98b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -265,6 +265,14 @@
     }
 
     if (inliner.neverInline(invoke, resolutionResult, singleTarget, whyAreYouNotInliningReporter)) {
+      if (singleTarget.getDefinition().getOptimizationInfo().forceInline()) {
+        throw new Unreachable(
+            "Unexpected attempt to force inline method `"
+                + singleTarget.toSourceString()
+                + "` in `"
+                + context.toSourceString()
+                + "`.");
+      }
       return null;
     }
 
@@ -435,7 +443,7 @@
     // Allow inlining a constructor into a constructor of the same class, as the constructor code
     // is expected to adhere to the VM specification.
     DexType callerMethodHolder = method.getHolderType();
-    DexType calleeMethodHolder = inlinee.method().holder();
+    DexType calleeMethodHolder = inlinee.method().getHolderType();
     // Calling a constructor on the same class from a constructor can always be inlined.
     if (method.getDefinition().isInstanceInitializer()
         && callerMethodHolder == calleeMethodHolder) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 3abbf9b..b26bb34 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -7,7 +7,6 @@
 import static com.google.common.base.Predicates.not;
 
 import com.android.tools.r8.androidapi.AvailableApiExceptions;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
@@ -125,11 +124,6 @@
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     AppInfoWithLiveness appInfo = appView.appInfo();
     DexMethod singleTargetReference = singleTarget.getReference();
-    if (singleTarget.getDefinition().getOptimizationInfo().forceInline()
-        && appInfo.isNeverInlineMethod(singleTargetReference)) {
-      throw new Unreachable();
-    }
-
     if (appInfo.isPinned(singleTargetReference)) {
       whyAreYouNotInliningReporter.reportPinned();
       return true;
@@ -151,7 +145,7 @@
     if (appInfo.noSideEffects.containsKey(invoke.getInvokedMethod())
         || appInfo.noSideEffects.containsKey(resolutionResult.getResolvedMethod().getReference())
         || appInfo.noSideEffects.containsKey(singleTargetReference)) {
-      return true;
+      return !singleTarget.getDefinition().getOptimizationInfo().forceInline();
     }
 
     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
index 1941743..976e869 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
@@ -211,7 +211,7 @@
             resolutionResult, context, ResolutionResult::lookupInvokeStaticTarget);
     if (!allowStaticInterfaceMethodCalls && target != null) {
       // See b/120121170.
-      DexClass methodClass = appView.definitionFor(graphLens.lookupType(target.holder()));
+      DexClass methodClass = appView.definitionFor(graphLens.lookupType(target.getHolderType()));
       if (methodClass != null && methodClass.isInterface() && target.hasCode()) {
         return ConstraintWithTarget.NEVER;
       }
@@ -243,7 +243,7 @@
     DexEncodedMethod alternativeDexEncodedMethod =
         lookup.apply(resolutionResult, superContext, appView.appInfo());
     if (alternativeDexEncodedMethod != null
-        && alternativeDexEncodedMethod.holder() == superContext.type) {
+        && alternativeDexEncodedMethod.getHolderType() == superContext.type) {
       return alternativeDexEncodedMethod;
     }
     return null;
@@ -370,7 +370,7 @@
       // This will fail at runtime.
       return ConstraintWithTarget.NEVER;
     }
-    DexType resolvedHolder = graphLens.lookupType(resolvedMember.holder());
+    DexType resolvedHolder = graphLens.lookupType(resolvedMember.getHolderType());
     assert initialResolutionHolder != null;
     ConstraintWithTarget memberConstraintWithTarget =
         ConstraintWithTarget.deriveConstraint(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 91f7642..8ba58f1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -311,7 +311,11 @@
         // Insert the definition of the replacement.
         replacement.setPosition(position);
         if (block.hasCatchHandlers()) {
-          iterator.split(code, blocks).listIterator(code).add(replacement);
+          BasicBlock splitBlock = iterator.split(code, blocks, false);
+          splitBlock.listIterator(code).add(replacement);
+          assert !block.hasCatchHandlers();
+          assert splitBlock.hasCatchHandlers();
+          block.copyCatchHandlers(code, blocks, splitBlock, appView.options());
         } else {
           iterator.add(replacement);
         }
@@ -403,13 +407,17 @@
         } else {
           assert current.isStaticGet();
           replaceInstructionByInitClassIfPossible(
-              current, target.holder(), code, iterator, context);
+              current, target.getHolderType(), code, iterator, context);
         }
 
         // Insert the definition of the replacement.
         replacement.setPosition(position);
         if (block.hasCatchHandlers()) {
-          iterator.split(code, blocks).listIterator(code).add(replacement);
+          BasicBlock splitBlock = iterator.split(code, blocks, false);
+          splitBlock.listIterator(code).add(replacement);
+          assert !block.hasCatchHandlers();
+          assert splitBlock.hasCatchHandlers();
+          block.copyCatchHandlers(code, blocks, splitBlock, appView.options());
         } else {
           iterator.add(replacement);
         }
@@ -502,7 +510,7 @@
     }
 
     replaceInstructionByInitClassIfPossible(
-        current, field.holder(), code, iterator, code.context());
+        current, field.getHolderType(), code, iterator, code.context());
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
index 868bcc5..3745120 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -366,7 +366,7 @@
     }
 
     InstanceInitializerInfo instanceInitializerInfo =
-        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo();
+        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
     if (instanceInitializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
       killAllNonFinalActiveFields();
     }
@@ -419,7 +419,7 @@
       // that we are conservative.
       activeState.removeNonFinalInstanceFields(field);
     } else if (instruction.isStaticPut()) {
-      if (field.holder != code.method().holder()) {
+      if (field.holder != code.method().getHolderType()) {
         // Accessing a static field on a different object could cause <clinit> to run which
         // could modify any static field on any other object.
         activeState.clearNonFinalStaticFields();
@@ -427,7 +427,7 @@
         activeState.removeNonFinalStaticField(field);
       }
     } else if (instruction.isStaticGet()) {
-      if (field.holder != code.method().holder()) {
+      if (field.holder != code.method().getHolderType()) {
         // Accessing a static field on a different object could cause <clinit> to run which
         // could modify any static field on any other object.
         activeState.clearNonFinalStaticFields();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index c11c8bf..48002cc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -28,9 +28,9 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -54,7 +54,7 @@
     private final Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod;
 
     UninstantiatedTypeOptimizationGraphLens(
-        BiMap<DexMethod, DexMethod> methodMap,
+        BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMap,
         Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod,
         AppView<?> appView) {
       super(
@@ -62,7 +62,7 @@
           methodMap,
           ImmutableMap.of(),
           null,
-          methodMap.inverse(),
+          methodMap.getInverseOneToOneMap(),
           appView.graphLens(),
           appView.dexItemFactory());
       this.appView = appView;
@@ -129,7 +129,8 @@
     }
 
     Map<Wrapper<DexMethod>, Set<DexType>> changedVirtualMethods = new HashMap<>();
-    BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+    BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+        new BidirectionalOneToOneHashMap<>();
     Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod = new IdentityHashMap<>();
 
     TopDownClassHierarchyTraversal.forProgramClasses(appView)
@@ -139,7 +140,7 @@
                 processClass(
                     clazz,
                     changedVirtualMethods,
-                    methodMapping,
+                    methodMapping.getForwardBacking(),
                     methodPoolCollection,
                     removedArgumentsInfoPerMethod));
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index 6270034..16fb558 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -26,9 +26,9 @@
 import com.android.tools.r8.utils.SymbolGenerationUtils.MixedCasing;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
@@ -48,7 +48,8 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final MethodPoolCollection methodPoolCollection;
 
-  private final BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+  private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+      new BidirectionalOneToOneHashMap<>();
   private final Map<DexMethod, ArgumentInfoCollection> removedArguments = new IdentityHashMap<>();
 
   public static class UnusedArgumentsGraphLens extends NestedGraphLens {
@@ -60,7 +61,7 @@
         Map<DexMethod, DexMethod> methodMap,
         Map<DexField, DexField> fieldMap,
         BiMap<DexField, DexField> originalFieldSignatures,
-        BiMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory,
         Map<DexMethod, ArgumentInfoCollection> removedArguments) {
@@ -111,7 +112,7 @@
           methodMapping,
           ImmutableMap.of(),
           ImmutableBiMap.of(),
-          methodMapping.inverse(),
+          methodMapping.getInverseOneToOneMap(),
           appView.graphLens(),
           appView.dexItemFactory(),
           removedArguments);
@@ -153,7 +154,8 @@
           // Constructors must be named `<init>`.
           return null;
         }
-        newSignature = appView.dexItemFactory().createMethod(method.holder(), newProto, newName);
+        newSignature =
+            appView.dexItemFactory().createMethod(method.getHolderType(), newProto, newName);
         count++;
       } while (!isMethodSignatureAvailable(newSignature));
       return newSignature;
@@ -193,7 +195,8 @@
           // Constructors must be named `<init>`.
           return null;
         }
-        newSignature = appView.dexItemFactory().createMethod(method.holder(), newProto, newName);
+        newSignature =
+            appView.dexItemFactory().createMethod(method.getHolderType(), newProto, newName);
         count++;
       } while (methodPool.hasSeen(equivalence.wrap(newSignature)));
       return newSignature;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 550a0e1..e080808 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -826,7 +826,7 @@
 
     // Check that the `eligibleInstance` does not escape via the constructor.
     InstanceInitializerInfo instanceInitializerInfo =
-        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo();
+        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
     if (instanceInitializerInfo.receiverMayEscapeOutsideConstructorChain()) {
       return null;
     }
@@ -856,7 +856,11 @@
           NopWhyAreYouNotInliningReporter.getInstance())) {
         return null;
       }
-      parent = encodedParentMethod.getOptimizationInfo().getInstanceInitializerInfo().getParent();
+      parent =
+          encodedParentMethod
+              .getOptimizationInfo()
+              .getContextInsensitiveInstanceInitializerInfo()
+              .getParent();
     }
 
     return new InliningInfo(singleTarget, eligibleClass.type);
@@ -1317,7 +1321,7 @@
       return false;
     }
     InstanceInitializerInfo initializerInfo =
-        definition.getOptimizationInfo().getInstanceInitializerInfo();
+        definition.getOptimizationInfo().getContextInsensitiveInstanceInitializerInfo();
     return initializerInfo.receiverNeverEscapesOutsideConstructorChain();
   }
 
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 b46805b..2d46582 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
@@ -604,7 +604,7 @@
         constraint = Constraint.NEVER;
         return;
       }
-      DexType resolvedHolder = target.holder();
+      DexType resolvedHolder = target.getHolderType();
       if (initialResolutionHolder == null) {
         constraint = Constraint.NEVER;
         return;
@@ -698,7 +698,7 @@
               hasInstanceInitializer = true;
               if (directMethod
                   .getOptimizationInfo()
-                  .getInstanceInitializerInfo()
+                  .getContextInsensitiveInstanceInitializerInfo()
                   .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
                 markEnumAsUnboxable(Reason.INVALID_INIT, enumClass);
                 break;
@@ -714,6 +714,7 @@
           }
 
           if (enumClass.classInitializationMayHaveSideEffects(appView)) {
+            enumClass.classInitializationMayHaveSideEffects(appView);
             markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
           }
         });
@@ -739,7 +740,8 @@
       DexClass dexClass = singleTarget.getHolder();
       if (dexClass.isProgramClass()) {
         if (dexClass.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
-          if (code.method().holder() == dexClass.type && code.method().isClassInitializer()) {
+          if (code.method().getHolderType() == dexClass.type
+              && code.method().isClassInitializer()) {
             // The enum instance initializer is allowed to be called only from the enum clinit.
             return Reason.ELIGIBLE;
           } else {
@@ -807,7 +809,8 @@
         return Reason.ELIGIBLE;
       } else if (singleTargetReference == factory.enumMembers.constructor) {
         // Enum constructor call is allowed only if called from an enum initializer.
-        if (code.method().isInstanceInitializer() && code.method().holder() == enumClass.type) {
+        if (code.method().isInstanceInitializer()
+            && code.method().getHolderType() == enumClass.type) {
           return Reason.ELIGIBLE;
         }
       }
@@ -823,7 +826,8 @@
       if (field == null) {
         return Reason.INVALID_FIELD_PUT;
       }
-      DexProgramClass dexClass = appView.programDefinitionFor(field.holder(), code.context());
+      DexProgramClass dexClass =
+          appView.programDefinitionFor(field.getHolderType(), code.context());
       if (dexClass == null) {
         return Reason.INVALID_FIELD_PUT;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index d99bf3e..b6660fc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableMap;
@@ -30,7 +31,7 @@
       Map<DexMethod, DexMethod> methodMap,
       Map<DexField, DexField> fieldMap,
       BiMap<DexField, DexField> originalFieldSignatures,
-      BiMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
       GraphLens previousLens,
       DexItemFactory dexItemFactory,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod,
@@ -76,7 +77,8 @@
 
     protected final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
     protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
 
     private Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
         new IdentityHashMap<>();
@@ -148,7 +150,7 @@
       }
       return new EnumUnboxingLens(
           typeMap,
-          originalMethodSignatures.inverse(),
+          originalMethodSignatures.getInverseOneToOneMap(),
           originalFieldSignatures.inverse(),
           originalFieldSignatures,
           originalMethodSignatures,
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 bfb520d..c456bd0 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
@@ -579,7 +579,8 @@
     // We compute encodedMembers by types.
     for (T encodedMember : encodedMembers) {
       List<T> members =
-          encodedMembersMap.computeIfAbsent(encodedMember.holder(), ignored -> new ArrayList<>());
+          encodedMembersMap.computeIfAbsent(
+              encodedMember.getHolderType(), ignored -> new ArrayList<>());
       members.add(encodedMember);
     }
     // We make the order deterministic.
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 1dd8e0c..92ce5a1 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
@@ -107,7 +107,7 @@
                 + method.name.toString());
     DexProto proto = encodedMethod.isStatic() ? method.proto : factory.prependHolderToProto(method);
     DexMethod newMethod = factory.createMethod(newHolder, fixupProto(proto), newMethodName);
-    assert appView.definitionFor(encodedMethod.holder()).lookupMethod(newMethod) == null;
+    assert appView.definitionFor(encodedMethod.getHolderType()).lookupMethod(newMethod) == null;
     lensBuilder.move(method, newMethod, encodedMethod.isStatic(), true);
     encodedMethod.accessFlags.promoteToPublic();
     encodedMethod.accessFlags.promoteToStatic();
@@ -145,7 +145,7 @@
   }
 
   private DexMethod ensureUniqueMethod(DexEncodedMethod encodedMethod, DexMethod newMethod) {
-    DexClass holder = appView.definitionFor(encodedMethod.holder());
+    DexClass holder = appView.definitionFor(encodedMethod.getHolderType());
     assert holder != null;
     if (newMethod.isInstanceInitializer(appView.dexItemFactory())) {
       newMethod =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index f1c2bdd..033a233 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -228,16 +228,9 @@
         continue;
       }
 
-      Int2IntMap ordinalToTargetMap = new Int2IntArrayMap(switchInsn.numberOfKeys());
-      for (int i = 0; i < switchInsn.numberOfKeys(); i++) {
-        assert switchInsn.targetBlockIndices()[i] != switchInsn.getFallthroughBlockIndex();
-        DexField field = info.indexMap.get(switchInsn.getKey(i));
-        EnumValueInfo valueInfo = info.valueInfoMap.getEnumValueInfo(field);
-        if (valueInfo != null) {
-          ordinalToTargetMap.put(valueInfo.ordinal, switchInsn.targetBlockIndices()[i]);
-        } else {
-          // The switch map refers to a field on the enum that does not exist in this compilation.
-        }
+      Int2IntMap ordinalToTargetMap = computeOrdinalToTargetMap(switchInsn, info);
+      if (ordinalToTargetMap == null) {
+        continue;
       }
 
       int fallthroughBlockIndex = switchInsn.getFallthroughBlockIndex();
@@ -322,6 +315,24 @@
     }
   }
 
+  private Int2IntMap computeOrdinalToTargetMap(IntSwitch switchInsn, EnumSwitchInfo info) {
+    Int2IntMap ordinalToTargetMap = new Int2IntArrayMap(switchInsn.numberOfKeys());
+    for (int i = 0; i < switchInsn.numberOfKeys(); i++) {
+      assert switchInsn.targetBlockIndices()[i] != switchInsn.getFallthroughBlockIndex();
+      DexField field = info.indexMap.get(switchInsn.getKey(i));
+      EnumValueInfo valueInfo = info.valueInfoMap.getEnumValueInfo(field);
+      if (valueInfo != null) {
+        if (appView.appInfo().isPinned(field)) {
+          return null;
+        }
+        ordinalToTargetMap.put(valueInfo.ordinal, switchInsn.targetBlockIndices()[i]);
+      } else {
+        // The switch map refers to a field on the enum that does not exist in this compilation.
+      }
+    }
+    return ordinalToTargetMap;
+  }
+
   private static final class EnumSwitchInfo {
 
     final DexType enumClass;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
index 3d20843..33b7242 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
@@ -80,7 +80,8 @@
     int size = method.method.getArity() + argOffset;
     TypeElement[] staticTypes = new TypeElement[size];
     if (!method.isStatic()) {
-      staticTypes[0] = TypeElement.fromDexType(method.holder(), definitelyNotNull(), appView);
+      staticTypes[0] =
+          TypeElement.fromDexType(method.getHolderType(), definitelyNotNull(), appView);
     }
     for (int i = 0; i < method.method.getArity(); i++) {
       staticTypes[i + argOffset] =
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 0e3e339..b2b10ae 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
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
@@ -82,7 +83,12 @@
   }
 
   @Override
-  public InstanceInitializerInfo getInstanceInitializerInfo() {
+  public InstanceInitializerInfo getContextInsensitiveInstanceInitializerInfo() {
+    return DefaultInstanceInitializerInfo.getInstance();
+  }
+
+  @Override
+  public InstanceInitializerInfo getInstanceInitializerInfo(InvokeDirect invoke) {
     return DefaultInstanceInitializerInfo.getInstance();
   }
 
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 f919a1e..cf7c4ee 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
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
@@ -68,7 +69,9 @@
 
   public abstract Set<DexType> getInitializedClassesOnNormalExit();
 
-  public abstract InstanceInitializerInfo getInstanceInitializerInfo();
+  public abstract InstanceInitializerInfo getContextInsensitiveInstanceInitializerInfo();
+
+  public abstract InstanceInitializerInfo getInstanceInitializerInfo(InvokeDirect invoke);
 
   public abstract boolean isInitializerEnablingJavaVmAssertions();
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index be88541..09c4a5d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -23,6 +23,7 @@
 import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
 import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_OF;
 import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
+import static com.android.tools.r8.ir.code.Opcodes.INT_SWITCH;
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
 import static com.android.tools.r8.ir.code.Opcodes.INVOKE_NEW_ARRAY;
@@ -38,6 +39,7 @@
 import static com.android.tools.r8.ir.code.Opcodes.SHL;
 import static com.android.tools.r8.ir.code.Opcodes.SHR;
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
+import static com.android.tools.r8.ir.code.Opcodes.STRING_SWITCH;
 import static com.android.tools.r8.ir.code.Opcodes.SUB;
 import static com.android.tools.r8.ir.code.Opcodes.THROW;
 import static com.android.tools.r8.ir.code.Opcodes.USHR;
@@ -91,8 +93,8 @@
 import com.android.tools.r8.ir.optimize.info.ParameterUsagesInfo.ParameterUsageBuilder;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeAnalyzer;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
-import com.android.tools.r8.ir.optimize.info.initializer.DefaultInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.ir.optimize.info.initializer.NonTrivialInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.typechecks.CheckCastAndInstanceOfMethodSpecialization;
 import com.android.tools.r8.kotlin.Kotlin;
@@ -435,11 +437,8 @@
     NonTrivialInstanceInitializerInfo.Builder builder =
         NonTrivialInstanceInitializerInfo.builder(instanceFieldInitializationInfos);
     InstanceInitializerInfo instanceInitializerInfo = analyzeInstanceInitializer(code, builder);
-    feedback.setInstanceInitializerInfo(
-        method,
-        instanceInitializerInfo != null
-            ? instanceInitializerInfo
-            : DefaultInstanceInitializerInfo.getInstance());
+    feedback.setInstanceInitializerInfoCollection(
+        method, InstanceInitializerInfoCollection.of(instanceInitializerInfo));
   }
 
   // This method defines trivial instance initializer as follows:
@@ -484,6 +483,8 @@
             break;
 
           case IF:
+          case INT_SWITCH:
+          case STRING_SWITCH:
             builder.setInstanceFieldInitializationMayDependOnEnvironment();
             break;
 
@@ -573,7 +574,7 @@
                 return null;
               }
               if (singleTarget.isInstanceInitializer() && invoke.getReceiver() == receiver) {
-                if (builder.hasParent()) {
+                if (builder.hasParent() && builder.getParent() != singleTarget.getReference()) {
                   return null;
                 }
                 // java.lang.Enum.<init>() and java.lang.Object.<init>() are considered trivial.
@@ -582,7 +583,8 @@
                   builder.setParent(invokedMethod);
                   break;
                 }
-                builder.merge(singleTarget.getOptimizationInfo().getInstanceInitializerInfo());
+                builder.merge(
+                    singleTarget.getOptimizationInfo().getInstanceInitializerInfo(invoke));
                 for (int i = 1; i < invoke.arguments().size(); i++) {
                   Value argument =
                       invoke.arguments().get(i).getAliasedValue(aliasesThroughAssumeAndCheckCasts);
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 07b7ce1..50cb793 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
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.AppInfoWithLivenessModifier;
 import com.android.tools.r8.utils.IteratorUtils;
@@ -254,10 +254,11 @@
   }
 
   @Override
-  public synchronized void setInstanceInitializerInfo(
-      DexEncodedMethod method, InstanceInitializerInfo instanceInitializerInfo) {
+  public synchronized void setInstanceInitializerInfoCollection(
+      DexEncodedMethod method,
+      InstanceInitializerInfoCollection instanceInitializerInfoCollection) {
     getMethodOptimizationInfoForUpdating(method)
-        .setInstanceInitializerInfo(instanceInitializerInfo);
+        .setInstanceInitializerInfoCollection(instanceInitializerInfoCollection);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index 6a28331..12fda15 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.BitSet;
 import java.util.Set;
@@ -115,8 +115,9 @@
       DexEncodedMethod method, ClassInlinerEligibilityInfo eligibility) {}
 
   @Override
-  public void setInstanceInitializerInfo(
-      DexEncodedMethod method, InstanceInitializerInfo instanceInitializerInfo) {}
+  public void setInstanceInitializerInfoCollection(
+      DexEncodedMethod method,
+      InstanceInitializerInfoCollection instanceInitializerInfoCollection) {}
 
   @Override
   public void setInitializerEnablingJavaVmAssertions(DexEncodedMethod method) {}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 9a3bb51..5840ea6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.BitSet;
 import java.util.Set;
@@ -165,9 +165,12 @@
   }
 
   @Override
-  public void setInstanceInitializerInfo(
-      DexEncodedMethod method, InstanceInitializerInfo instanceInitializerInfo) {
-    method.getMutableOptimizationInfo().setInstanceInitializerInfo(instanceInitializerInfo);
+  public void setInstanceInitializerInfoCollection(
+      DexEncodedMethod method,
+      InstanceInitializerInfoCollection instanceInitializerInfoCollection) {
+    method
+        .getMutableOptimizationInfo()
+        .setInstanceInitializerInfoCollection(instanceInitializerInfoCollection);
   }
 
   @Override
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/UpdatableMethodOptimizationInfo.java
index 6d9fc9f..877c72d 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/UpdatableMethodOptimizationInfo.java
@@ -12,11 +12,12 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInlinerEligibilityInfo;
 import com.android.tools.r8.ir.optimize.info.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
-import com.android.tools.r8.ir.optimize.info.initializer.DefaultInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
 import java.util.BitSet;
@@ -39,8 +40,8 @@
   private BridgeInfo bridgeInfo = null;
   private ClassInlinerEligibilityInfo classInlinerEligibility =
       DefaultMethodOptimizationInfo.UNKNOWN_CLASS_INLINER_ELIGIBILITY;
-  private InstanceInitializerInfo instanceInitializerInfo =
-      DefaultInstanceInitializerInfo.getInstance();
+  private InstanceInitializerInfoCollection instanceInitializerInfoCollection =
+      InstanceInitializerInfoCollection.empty();
   private ParameterUsagesInfo parametersUsages =
       DefaultMethodOptimizationInfo.UNKNOWN_PARAMETER_USAGE_INFO;
   // Stores information about nullability hint per parameter. If set, that means, the method
@@ -138,7 +139,7 @@
     inlining = template.inlining;
     bridgeInfo = template.bridgeInfo;
     classInlinerEligibility = template.classInlinerEligibility;
-    instanceInitializerInfo = template.instanceInitializerInfo;
+    instanceInitializerInfoCollection = template.instanceInitializerInfoCollection;
     parametersUsages = template.parametersUsages;
     nonNullParamOrThrow = template.nonNullParamOrThrow;
     nonNullParamOnNormalExits = template.nonNullParamOnNormalExits;
@@ -172,9 +173,8 @@
 
   public UpdatableMethodOptimizationInfo fixupInstanceInitializerInfo(
       AppView<AppInfoWithLiveness> appView, GraphLens lens) {
-    if (instanceInitializerInfo != null) {
-      instanceInitializerInfo = instanceInitializerInfo.rewrittenWithLens(appView, lens);
-    }
+    instanceInitializerInfoCollection =
+        instanceInitializerInfoCollection.rewrittenWithLens(appView, lens);
     return this;
   }
 
@@ -248,8 +248,13 @@
   }
 
   @Override
-  public InstanceInitializerInfo getInstanceInitializerInfo() {
-    return instanceInitializerInfo;
+  public InstanceInitializerInfo getContextInsensitiveInstanceInitializerInfo() {
+    return instanceInitializerInfoCollection.getContextInsensitive();
+  }
+
+  @Override
+  public InstanceInitializerInfo getInstanceInitializerInfo(InvokeDirect invoke) {
+    return instanceInitializerInfoCollection.get(invoke);
   }
 
   @Override
@@ -371,8 +376,9 @@
     this.classInlinerEligibility = eligibility;
   }
 
-  void setInstanceInitializerInfo(InstanceInitializerInfo instanceInitializerInfo) {
-    this.instanceInitializerInfo = instanceInitializerInfo;
+  void setInstanceInitializerInfoCollection(
+      InstanceInitializerInfoCollection instanceInitializerInfoCollection) {
+    this.instanceInitializerInfoCollection = instanceInitializerInfoCollection;
   }
 
   void setInitializerEnablingJavaAssertions() {
@@ -520,7 +526,7 @@
     // classInlinerEligibility: chances are the method is not an instance method anymore.
     classInlinerEligibility = DefaultMethodOptimizationInfo.UNKNOWN_CLASS_INLINER_ELIGIBILITY;
     // initializerInfo: the computed initializer info may become invalid.
-    instanceInitializerInfo = null;
+    instanceInitializerInfoCollection = InstanceInitializerInfoCollection.empty();
     // initializerEnablingJavaAssertions: `this` could trigger <clinit> of the previous holder.
     setFlag(
         INITIALIZER_ENABLING_JAVA_ASSERTIONS_FLAG,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/AlwaysTrueInstanceInitializerInfoContext.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/AlwaysTrueInstanceInitializerInfoContext.java
new file mode 100644
index 0000000..12ef51a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/AlwaysTrueInstanceInitializerInfoContext.java
@@ -0,0 +1,29 @@
+// 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.info.initializer;
+
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+public class AlwaysTrueInstanceInitializerInfoContext extends InstanceInitializerInfoContext {
+
+  private static final AlwaysTrueInstanceInitializerInfoContext INSTANCE =
+      new AlwaysTrueInstanceInitializerInfoContext();
+
+  private AlwaysTrueInstanceInitializerInfoContext() {}
+
+  public static AlwaysTrueInstanceInitializerInfoContext getInstance() {
+    return INSTANCE;
+  }
+
+  @Override
+  public boolean isAlwaysTrue() {
+    return true;
+  }
+
+  @Override
+  public boolean isSatisfiedBy(InvokeMethod invoke) {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextInsensitiveInstanceInitializerInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextInsensitiveInstanceInitializerInfoCollection.java
new file mode 100644
index 0000000..6cf070d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextInsensitiveInstanceInitializerInfoCollection.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info.initializer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class ContextInsensitiveInstanceInitializerInfoCollection
+    extends InstanceInitializerInfoCollection {
+
+  private final NonTrivialInstanceInitializerInfo info;
+
+  ContextInsensitiveInstanceInitializerInfoCollection(NonTrivialInstanceInitializerInfo info) {
+    this.info = info;
+  }
+
+  @Override
+  public NonTrivialInstanceInitializerInfo getContextInsensitive() {
+    return info;
+  }
+
+  @Override
+  public NonTrivialInstanceInitializerInfo get(InvokeDirect invoke) {
+    return info;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return false;
+  }
+
+  @Override
+  public ContextInsensitiveInstanceInitializerInfoCollection rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens lens) {
+    NonTrivialInstanceInitializerInfo rewrittenInfo = info.rewrittenWithLens(appView, lens);
+    if (rewrittenInfo != info) {
+      return new ContextInsensitiveInstanceInitializerInfoCollection(rewrittenInfo);
+    }
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextSensitiveInstanceInitializerInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextSensitiveInstanceInitializerInfoCollection.java
new file mode 100644
index 0000000..3347587
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/ContextSensitiveInstanceInitializerInfoCollection.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info.initializer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map.Entry;
+
+public class ContextSensitiveInstanceInitializerInfoCollection
+    extends InstanceInitializerInfoCollection {
+
+  private final ImmutableMap<InstanceInitializerInfoContext, NonTrivialInstanceInitializerInfo>
+      infos;
+
+  protected ContextSensitiveInstanceInitializerInfoCollection(
+      ImmutableMap<InstanceInitializerInfoContext, NonTrivialInstanceInitializerInfo> infos) {
+    assert !infos.isEmpty();
+    this.infos = infos;
+  }
+
+  @Override
+  public InstanceInitializerInfo getContextInsensitive() {
+    NonTrivialInstanceInitializerInfo result =
+        infos.get(AlwaysTrueInstanceInitializerInfoContext.getInstance());
+    return result != null ? result : DefaultInstanceInitializerInfo.getInstance();
+  }
+
+  @Override
+  public InstanceInitializerInfo get(InvokeDirect invoke) {
+    assert infos.keySet().stream().filter(context -> context.isSatisfiedBy(invoke)).count() <= 1;
+    for (Entry<InstanceInitializerInfoContext, NonTrivialInstanceInitializerInfo> entry :
+        infos.entrySet()) {
+      if (entry.getKey().isSatisfiedBy(invoke)) {
+        return entry.getValue();
+      }
+    }
+    return DefaultInstanceInitializerInfo.getInstance();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return false;
+  }
+
+  @Override
+  public InstanceInitializerInfoCollection rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens lens) {
+    Builder builder = builder();
+    infos.forEach((context, info) -> builder.put(context, info.rewrittenWithLens(appView, lens)));
+    return builder.build();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
index c9a7f32..af59b7c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
@@ -25,6 +25,11 @@
   }
 
   @Override
+  public boolean isDefaultInstanceInitializerInfo() {
+    return true;
+  }
+
+  @Override
   public DexMethod getParent() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/EmptyInstanceInitializerInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/EmptyInstanceInitializerInfoCollection.java
new file mode 100644
index 0000000..d7d3560
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/EmptyInstanceInitializerInfoCollection.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info.initializer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class EmptyInstanceInitializerInfoCollection extends InstanceInitializerInfoCollection {
+
+  private static final EmptyInstanceInitializerInfoCollection EMPTY =
+      new EmptyInstanceInitializerInfoCollection();
+
+  private EmptyInstanceInitializerInfoCollection() {}
+
+  public static EmptyInstanceInitializerInfoCollection getInstance() {
+    return EMPTY;
+  }
+
+  @Override
+  public DefaultInstanceInitializerInfo getContextInsensitive() {
+    return DefaultInstanceInitializerInfo.getInstance();
+  }
+
+  @Override
+  public DefaultInstanceInitializerInfo get(InvokeDirect invoke) {
+    return DefaultInstanceInitializerInfo.getInstance();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return true;
+  }
+
+  @Override
+  public EmptyInstanceInitializerInfoCollection rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens lens) {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
index 1e36f40..3865bc1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
@@ -13,6 +13,18 @@
 
 public abstract class InstanceInitializerInfo {
 
+  public boolean isDefaultInstanceInitializerInfo() {
+    return false;
+  }
+
+  public boolean isNonTrivialInstanceInitializerInfo() {
+    return false;
+  }
+
+  public NonTrivialInstanceInitializerInfo asNonTrivialInstanceInitializerInfo() {
+    return null;
+  }
+
   public abstract DexMethod getParent();
 
   public abstract InstanceFieldInitializationInfoCollection fieldInitializationInfos();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoCollection.java
new file mode 100644
index 0000000..ce53a6a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoCollection.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info.initializer;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MapUtils;
+import com.google.common.collect.ImmutableMap;
+
+public abstract class InstanceInitializerInfoCollection {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static InstanceInitializerInfoCollection empty() {
+    return EmptyInstanceInitializerInfoCollection.getInstance();
+  }
+
+  public static InstanceInitializerInfoCollection of(InstanceInitializerInfo info) {
+    if (info != null && info.isNonTrivialInstanceInitializerInfo()) {
+      return new ContextInsensitiveInstanceInitializerInfoCollection(
+          info.asNonTrivialInstanceInitializerInfo());
+    }
+    return empty();
+  }
+
+  public abstract InstanceInitializerInfo getContextInsensitive();
+
+  public abstract InstanceInitializerInfo get(InvokeDirect invoke);
+
+  public abstract boolean isEmpty();
+
+  public abstract InstanceInitializerInfoCollection rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, GraphLens lens);
+
+  public static class Builder {
+
+    private final ImmutableMap.Builder<
+            InstanceInitializerInfoContext, NonTrivialInstanceInitializerInfo>
+        infosBuilder = ImmutableMap.builder();
+
+    private Builder() {}
+
+    public Builder put(InstanceInitializerInfoContext context, InstanceInitializerInfo info) {
+      if (info.isNonTrivialInstanceInitializerInfo()) {
+        infosBuilder.put(context, info.asNonTrivialInstanceInitializerInfo());
+      }
+      return this;
+    }
+
+    public InstanceInitializerInfoCollection build() {
+      ImmutableMap<InstanceInitializerInfoContext, NonTrivialInstanceInitializerInfo> infos =
+          infosBuilder.build();
+      if (infos.isEmpty()) {
+        return empty();
+      }
+      if (infos.size() == 1 && MapUtils.firstKey(infos).isAlwaysTrue()) {
+        return new ContextInsensitiveInstanceInitializerInfoCollection(MapUtils.firstValue(infos));
+      }
+      return new ContextSensitiveInstanceInitializerInfoCollection(infos);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoContext.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoContext.java
new file mode 100644
index 0000000..8c28004
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfoContext.java
@@ -0,0 +1,16 @@
+// 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.info.initializer;
+
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+public abstract class InstanceInitializerInfoContext {
+
+  public boolean isAlwaysTrue() {
+    return false;
+  }
+
+  public abstract boolean isSatisfiedBy(InvokeMethod invoke);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
index ee08c65..40b3edc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
@@ -38,6 +38,16 @@
     this.parent = parent;
   }
 
+  @Override
+  public boolean isNonTrivialInstanceInitializerInfo() {
+    return true;
+  }
+
+  @Override
+  public NonTrivialInstanceInitializerInfo asNonTrivialInstanceInitializerInfo() {
+    return this;
+  }
+
   private static boolean verifyNoUnknownBits(int data) {
     int knownBits =
         INSTANCE_FIELD_INITIALIZATION_INDEPENDENT_OF_ENVIRONMENT
@@ -189,8 +199,12 @@
       return parent != null;
     }
 
+    public DexMethod getParent() {
+      return parent;
+    }
+
     public Builder setParent(DexMethod parent) {
-      assert !hasParent();
+      assert !hasParent() || getParent() == parent;
       this.parent = parent;
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
index d0412b7..9b41f18 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.optimize.info.LibraryOptimizationInfoInitializerFeedback;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoCollection;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.ir.optimize.info.initializer.NonTrivialInstanceInitializerInfo;
 import com.google.common.collect.Sets;
 import java.util.BitSet;
@@ -67,11 +68,12 @@
               .recordInitializationInfo(
                   enumMembers.ordinalField, factory.createArgumentInitializationInfo(2))
               .build();
-      feedback.setInstanceInitializerInfo(
+      feedback.setInstanceInitializerInfoCollection(
           enumConstructor,
-          NonTrivialInstanceInitializerInfo.builder(fieldInitializationInfos)
-              .setParent(dexItemFactory.objectMembers.constructor)
-              .build());
+          InstanceInitializerInfoCollection.of(
+              NonTrivialInstanceInitializerInfo.builder(fieldInitializationInfos)
+                  .setParent(dexItemFactory.objectMembers.constructor)
+                  .build()));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/BasicBlockMuncher.java b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/BasicBlockMuncher.java
index fc48072..b196de9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/BasicBlockMuncher.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/BasicBlockMuncher.java
@@ -5,13 +5,14 @@
 package com.android.tools.r8.ir.optimize.peepholes;
 
 import static com.android.tools.r8.utils.InternalOptions.TestingOptions.NO_LIMIT;
+import static com.android.tools.r8.utils.PredicateUtils.not;
 
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.LinearFlowInstructionListIterator;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import java.util.ListIterator;
@@ -45,7 +46,7 @@
     int iterations = 0;
     while (blocksIterator.hasPrevious()) {
       BasicBlock currentBlock = blocksIterator.previous();
-      InstructionListIterator it =
+      LinearFlowInstructionListIterator it =
           new LinearFlowInstructionListIterator(
               code, currentBlock, currentBlock.getInstructions().size());
       boolean matched = false;
@@ -74,6 +75,11 @@
               new LinearFlowInstructionListIterator(
                   code, currentBlock, currentBlock.getInstructions().size());
         } else {
+          // Move the iterator to the first block we have not seen.
+          if (IteratorUtils.previousUntilUnsafe(blocksIterator, not(it::hasVisitedBlock)) != null) {
+            // Ensure that we visit the first not visited block.
+            blocksIterator.next();
+          }
           break;
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
index 0231d43..d5ef586 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -84,7 +84,7 @@
     }
 
     DexType hostType() {
-      return singletonField.holder();
+      return singletonField.getHolderType();
     }
 
     DexProgramClass hostClass() {
@@ -495,7 +495,7 @@
 
     if (ListUtils.lastIndexMatching(values, v -> v.getAliasedValue() == candidateValue) != 0
         || methodInvoked == null
-        || methodInvoked.holder() != info.candidate.type) {
+        || methodInvoked.getHolderType() != info.candidate.type) {
       return false;
     }
 
@@ -663,7 +663,7 @@
               : resolutionResult.isVirtualTarget() ? resolutionResult.getSingleTarget() : null;
       if (ListUtils.lastIndexMatching(invoke.inValues(), isAliasedValue) == 0
           && methodInvoked != null
-          && methodInvoked.holder() == candidateInfo.candidate.type) {
+          && methodInvoked.getHolderType() == candidateInfo.candidate.type) {
         return true;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
index 6eeeb46..fbc4c12 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableMap;
 
@@ -17,13 +18,13 @@
   ClassStaticizerGraphLens(
       AppView<?> appView,
       BiMap<DexField, DexField> fieldMapping,
-      BiMap<DexMethod, DexMethod> methodMapping) {
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping) {
     super(
         ImmutableMap.of(),
         methodMapping,
         fieldMapping,
         fieldMapping.inverse(),
-        methodMapping.inverse(),
+        methodMapping.getInverseOneToOneMap(),
         appView.graphLens(),
         appView.dexItemFactory());
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index f776d44..2313a5a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -45,6 +45,7 @@
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
@@ -737,7 +738,8 @@
   }
 
   private ProgramMethodSet staticizeMethodSymbols() {
-    BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+    BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+        new BidirectionalOneToOneHashMap<>();
     BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
 
     ProgramMethodSet staticizedMethods = ProgramMethodSet.create();
@@ -801,7 +803,7 @@
       DexProgramClass candidateClass,
       DexType hostType,
       DexProgramClass hostClass,
-      BiMap<DexMethod, DexMethod> methodMapping,
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping,
       BiMap<DexField, DexField> fieldMapping) {
     candidateToHostMapping.put(candidateClass.type, hostType);
 
@@ -856,7 +858,7 @@
         // has just been migrated to the host class.
         staticizedMethods.createAndAdd(hostClass, newMethod);
       }
-      DexMethod originalMethod = methodMapping.inverse().get(method.method);
+      DexMethod originalMethod = methodMapping.getRepresentativeKey(method.method);
       if (originalMethod == null) {
         methodMapping.put(method.method, newMethod.method);
       } else {
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 97ef2dc..58c9f2d 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -361,6 +361,13 @@
       LensCodeRewriterUtils rewriter,
       ClassWriter writer,
       ImmutableMap<DexString, DexValue> defaults) {
+    NamingLens namingLens = this.namingLens;
+
+    // For "pass through" classes which has already been library desugared use the identity lens.
+    if (appView.isAlreadyLibraryDesugared(method.getHolder())) {
+      namingLens = NamingLens.getIdentityLens();
+    }
+
     DexEncodedMethod definition = method.getDefinition();
     int access = definition.getAccessFlags().getAsCfAccessFlags();
     if (definition.isDeprecated()) {
@@ -383,7 +390,7 @@
     writeAnnotations(visitor::visitAnnotation, definition.annotations().annotations);
     writeParameterAnnotations(visitor, definition.parameterAnnotationsList);
     if (!definition.shouldNotHaveCode()) {
-      writeCode(method, classFileVersion, rewriter, visitor);
+      writeCode(method, classFileVersion, namingLens, rewriter, visitor);
     }
     visitor.visitEnd();
   }
@@ -520,6 +527,7 @@
   private void writeCode(
       ProgramMethod method,
       CfVersion classFileVersion,
+      NamingLens namingLens,
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
     CfCode code = method.getDefinition().getCode().asCfCode();
diff --git a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
index 86dac13..a8bd96c 100644
--- a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
@@ -104,7 +104,7 @@
     // Used for iterating the sub trees that has this node as root.
     final Set<DexType> children = new HashSet<>();
     // Collection of the frontier reservation types and the interface type itself.
-    final Set<DexType> reservationTypes = new HashSet<>();
+    private final Set<DexType> reservationTypes = new HashSet<>();
 
     InterfaceReservationState(DexClass iface) {
       this.iface = iface;
@@ -139,6 +139,10 @@
       return isReserved == null ? null : method.getName();
     }
 
+    void addReservationType(DexType type) {
+      this.reservationTypes.add(type);
+    }
+
     void reserveName(DexString reservedName, DexEncodedMethod method) {
       forAll(
           s -> {
@@ -162,7 +166,7 @@
                 }
                 return null;
               });
-      return result == null ? true : result;
+      return result == null || result;
     }
 
     void addRenaming(DexString newName, DexEncodedMethod method) {
@@ -304,7 +308,7 @@
                 }
                 return null;
               });
-      return result == null ? true : result;
+      return result == null || result;
     }
 
     void addRenaming(DexString newName, MethodNameMinifier.State minifierState) {
@@ -398,7 +402,7 @@
       assert iface.isInterface();
       minifierState.allocateReservationStateAndReserve(iface.type, iface.type);
       InterfaceReservationState iFaceState = new InterfaceReservationState(iface);
-      iFaceState.reservationTypes.add(iface.type);
+      iFaceState.addReservationType(iface.type);
       interfaceStateMap.put(iface.type, iFaceState);
     }
   }
@@ -644,8 +648,12 @@
                   InterfaceReservationState iState = interfaceStateMap.get(directlyImplemented);
                   if (iState != null) {
                     DexType frontierType = minifierState.getFrontier(clazz.type);
-                    assert minifierState.getReservationState(frontierType) != null;
-                    iState.reservationTypes.add(frontierType);
+                    iState.addReservationType(frontierType);
+                    // The reservation state should already be added, but if a class is extending
+                    // an interface, we will not visit the class during the sub-type traversel
+                    if (minifierState.getReservationState(clazz.type) == null) {
+                      minifierState.allocateReservationStateAndReserve(clazz.type, frontierType);
+                    }
                   }
                 }
               }
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index 9893fb7..4bc638a 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.SymbolGenerationUtils;
 import com.android.tools.r8.utils.SymbolGenerationUtils.MixedCasing;
 import com.android.tools.r8.utils.Timing;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -47,7 +48,7 @@
     assert appView.options().isMinifying();
     SubtypingInfo subtypingInfo = appView.appInfo().computeSubtypingInfo();
     timing.begin("ComputeInterfaces");
-    Set<DexClass> interfaces = new TreeSet<>((a, b) -> a.type.compareTo(b.type));
+    Set<DexClass> interfaces = new TreeSet<>(Comparator.comparing(a -> a.type));
     interfaces.addAll(appView.appInfo().computeReachableInterfaces());
     timing.end();
     timing.begin("MinifyClasses");
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 136595a..04a24b6 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -191,7 +191,7 @@
           if (target == null || target.method == method) {
             return;
           }
-          DexClass targetClass = appView.definitionFor(target.holder());
+          DexClass targetClass = appView.definitionFor(target.getHolderType());
           DexMethod targetMethod = target.method;
           if (originalClass.isProgramClass()) {
             // In Java bytecode, it is only possible to target interface methods that are in one of
@@ -284,7 +284,7 @@
   }
 
   private boolean mayNeedBridgeForVisibility(ProgramMethod context, DexEncodedMethod method) {
-    DexType holderType = method.holder();
+    DexType holderType = method.getHolderType();
     DexClass holder = appView.definitionFor(holderType);
     if (holder == null) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
index 52e7b3d..bd8db04 100644
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import java.util.Set;
@@ -25,7 +26,7 @@
         ImmutableMap.of(),
         ImmutableMap.of(),
         null,
-        null,
+        BidirectionalManyToManyRepresentativeMap.empty(),
         appView.graphLens(),
         appView.dexItemFactory());
     this.appView = appView;
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
index d3ceea5..b871193 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 
@@ -20,11 +21,11 @@
   private RepackagingLens(
       AppView<AppInfoWithLiveness> appView,
       BiMap<DexField, DexField> originalFieldSignatures,
-      BiMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
       BiMap<DexType, DexType> originalTypes) {
     super(
         originalTypes.inverse(),
-        originalMethodSignatures.inverse(),
+        originalMethodSignatures.getInverseBacking(),
         originalFieldSignatures.inverse(),
         originalFieldSignatures,
         originalMethodSignatures,
@@ -48,7 +49,8 @@
 
     protected final BiMap<DexType, DexType> originalTypes = HashBiMap.create();
     protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
 
     public void recordMove(DexField from, DexField to) {
       originalFieldSignatures.put(to, from);
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 bdda13b..7687ad4 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -819,7 +819,7 @@
     if (fieldAccessInfo == null || !fieldAccessInfo.isWritten()) {
       return false;
     }
-    DexType holder = field.holder();
+    DexType holder = field.getHolderType();
     return fieldAccessInfo.isWrittenOnlyInMethodSatisfying(
         method ->
             method.getDefinition().isInstanceInitializer() && method.getHolderType() == holder);
@@ -829,7 +829,7 @@
     assert checkIfObsolete();
     assert isFieldWritten(field) : "Expected field `" + field.toSourceString() + "` to be written";
     DexEncodedMethod staticInitializer =
-        definitionFor(field.holder()).asProgramClass().getClassInitializer();
+        definitionFor(field.getHolderType()).asProgramClass().getClassInitializer();
     return staticInitializer != null && isFieldOnlyWrittenInMethod(field, staticInitializer);
   }
 
@@ -849,7 +849,7 @@
   }
 
   private boolean isLibraryOrClasspathField(DexEncodedField field) {
-    DexClass holder = definitionFor(field.holder());
+    DexClass holder = definitionFor(field.getHolderType());
     return holder == null || holder.isLibraryClass() || holder.isClasspathClass();
   }
 
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 ed31df5..f6b5aa2 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -649,6 +649,33 @@
     }
   }
 
+  private void warnIfClassExtendsInterfaceOrImplementsClass(DexProgramClass clazz) {
+    if (clazz.superType != null) {
+      DexClass superClass = definitionFor(clazz.superType);
+      if (superClass != null && superClass.isInterface()) {
+        options.reporter.warning(
+            new StringDiagnostic(
+                "Class "
+                    + clazz.toSourceString()
+                    + " extends "
+                    + superClass.toSourceString()
+                    + " which is an interface"));
+      }
+    }
+    for (DexType iface : clazz.interfaces.values) {
+      DexClass ifaceClass = definitionFor(iface);
+      if (ifaceClass != null && !ifaceClass.isInterface()) {
+        options.reporter.warning(
+            new StringDiagnostic(
+                "Class "
+                    + clazz.toSourceString()
+                    + " implements "
+                    + ifaceClass.toSourceString()
+                    + " which is not an interface"));
+      }
+    }
+  }
+
   private void enqueueRootItems(Map<DexReference, Set<ProguardKeepRuleBase>> items) {
     items.entrySet().forEach(this::enqueueRootItem);
   }
@@ -759,13 +786,13 @@
       }
     } else if (item.isDexEncodedField()) {
       DexEncodedField field = item.asDexEncodedField();
-      DexProgramClass holder = getProgramClassOrNull(field.holder());
+      DexProgramClass holder = getProgramClassOrNull(field.getHolderType());
       if (holder != null) {
         enqueueRootField(new ProgramField(holder, field), rules, precondition);
       }
     } else if (item.isDexEncodedMethod()) {
       DexEncodedMethod method = item.asDexEncodedMethod();
-      DexProgramClass holder = getProgramClassOrNull(method.holder());
+      DexProgramClass holder = getProgramClassOrNull(method.getHolderType());
       if (holder != null) {
         enqueueRootMethod(new ProgramMethod(holder, method), rules, precondition);
       }
@@ -1459,6 +1486,10 @@
       // Must mark the field as targeted even if it does not exist.
       markFieldAsTargeted(fieldReference, currentMethod);
       noClassMerging.add(fieldReference.getHolderType());
+
+      // Record field reference for generated extension registry shrinking.
+      appView.withGeneratedExtensionRegistryShrinker(
+          shrinker -> shrinker.handleFailedOrUnknownFieldResolution(fieldReference, currentMethod));
       return;
     }
 
@@ -1477,17 +1508,15 @@
       Log.verbose(getClass(), "Register Sget `%s`.", fieldReference);
     }
 
-    if (appView.options().protoShrinking().enableGeneratedExtensionRegistryShrinking) {
-      // If it is a dead proto extension field, don't trace onwards.
-      boolean skipTracing =
-          appView.withGeneratedExtensionRegistryShrinker(
-              shrinker ->
-                  shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, keepInfo),
-              false);
-      if (skipTracing) {
-        addDeadProtoTypeCandidate(field.getHolder());
-        return;
-      }
+    // If it is a dead proto extension field, don't trace onwards.
+    boolean skipTracing =
+        appView.withGeneratedExtensionRegistryShrinker(
+            shrinker ->
+                shrinker.isDeadProtoExtensionField(field, fieldAccessInfoCollection, keepInfo),
+            false);
+    if (skipTracing) {
+      addDeadProtoTypeCandidate(field.getHolder());
+      return;
     }
 
     if (field.getReference() != fieldReference) {
@@ -1679,6 +1708,9 @@
       markTypeAsLive(holder.superType, reason);
     }
 
+    // Warn if the class extends an interface or implements a class
+    warnIfClassExtendsInterfaceOrImplementsClass(holder);
+
     // If this is an interface that has just become live, then report previously seen but unreported
     // implemented-by edges.
     transitionUnusedInterfaceToLive(holder);
@@ -2385,7 +2417,7 @@
           && appView.rewritePrefix.hasRewrittenTypeInSignature(method.method.proto, appView)) {
         DexMethod methodToResolve =
             DesugaredLibraryAPIConverter.methodWithVivifiedTypeInSignature(
-                method.method, method.holder(), appView);
+                method.method, method.getHolderType(), appView);
         assert methodToResolve != method.method;
         markLibraryOrClasspathOverrideLive(
             instantiation,
@@ -2787,7 +2819,7 @@
     failedResolutionTargets.add(symbolicMethod);
     failedResolution.forEachFailureDependency(
         method -> {
-          DexProgramClass clazz = getProgramClassOrNull(method.holder());
+          DexProgramClass clazz = getProgramClassOrNull(method.getHolderType());
           if (clazz != null) {
             failedResolutionTargets.add(method.method);
             markMethodAsTargeted(new ProgramMethod(clazz, method), reason);
@@ -2839,7 +2871,7 @@
       return;
     }
 
-    DexProgramClass clazz = getProgramClassOrNull(target.holder());
+    DexProgramClass clazz = getProgramClassOrNull(target.getHolderType());
     if (clazz == null) {
       return;
     }
@@ -3672,7 +3704,7 @@
         } else {
           DexEncodedMethod implementation = definition.getDefaultInterfaceMethodImplementation();
           if (implementation != null) {
-            DexProgramClass companion = getProgramClassOrNull(implementation.holder());
+            DexProgramClass companion = getProgramClassOrNull(implementation.getHolderType());
             markTypeAsLive(companion, graphReporter.reportCompanionClass(holder, companion));
             markVirtualMethodAsLive(
                 new ProgramMethod(companion, implementation),
diff --git a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
index b154081..d78c808 100644
--- a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
+++ b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
@@ -281,7 +281,7 @@
 
   public KeepReasonWitness reportCompanionMethod(
       DexEncodedMethod definition, DexEncodedMethod implementation) {
-    assert InterfaceMethodRewriter.isCompanionClassType(implementation.holder());
+    assert InterfaceMethodRewriter.isCompanionClassType(implementation.getHolderType());
     if (keptGraphConsumer == null) {
       return KeepReasonWitness.INSTANCE;
     }
@@ -356,7 +356,8 @@
     if (skipReporting(reason)) {
       return KeepReasonWitness.INSTANCE;
     }
-    if (reason.edgeKind() == EdgeKind.IsLibraryMethod && isNonProgramClass(method.holder())) {
+    if (reason.edgeKind() == EdgeKind.IsLibraryMethod
+        && isNonProgramClass(method.getHolderType())) {
       // Don't report edges to actual library methods.
       // TODO(b/120959039): This should be dead code once no library classes are ever enqueued.
       return KeepReasonWitness.INSTANCE;
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 9ccbf5e..85a7b88 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -306,13 +306,13 @@
 
     @Override
     public KeepMethodInfo getMethodInfo(DexEncodedMethod method, DexProgramClass holder) {
-      assert method.holder() == holder.type;
+      assert method.getHolderType() == holder.type;
       return keepMethodInfo.getOrDefault(method.method, KeepMethodInfo.bottom());
     }
 
     @Override
     public KeepFieldInfo getFieldInfo(DexEncodedField field, DexProgramClass holder) {
-      assert field.holder() == holder.type;
+      assert field.getHolderType() == holder.type;
       return keepFieldInfo.getOrDefault(field.field, KeepFieldInfo.bottom());
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepReason.java b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
index 31f7611..53b944f 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepReason.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
@@ -160,7 +160,7 @@
 
     private InvokedFrom(DexProgramClass holder, DexEncodedMethod method) {
       super(method);
-      assert holder.type == method.holder();
+      assert holder.type == method.getHolderType();
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
index 2de5c70..d63c685 100644
--- a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
@@ -229,7 +229,7 @@
       }
 
       InstanceInitializerInfo initializerInfo =
-          singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo();
+          singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
       return initializerInfo.receiverNeverEscapesOutsideConstructorChain();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
index 590ede0..42b56bd 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
@@ -57,7 +57,7 @@
   public static ProguardKeepRule buildMethodKeepRule(DexClass clazz, DexEncodedMethod method) {
     // TODO(b/122295241): These generated rules should be linked into the graph, eg, the method
     // using identified reflection should be the source keeping the target alive.
-    assert clazz.type == method.holder();
+    assert clazz.type == method.getHolderType();
     ProguardKeepRule.Builder builder = ProguardKeepRule.builder();
     builder.setOrigin(proguardCompatOrigin);
     builder.setType(ProguardKeepRuleType.KEEP_CLASS_MEMBERS);
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 1d476bf..be431d3 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -403,7 +403,7 @@
       DexEncodedMethod target =
           appView.appInfo().unsafeResolveMethodDueToDexFormat(referenceInSubType).getSingleTarget();
       // But, the resolution should not be landed on the current type we are visiting.
-      if (target == null || target.holder() == type) {
+      if (target == null || target.getHolderType() == type) {
         continue;
       }
       ProguardMemberRule ruleInSubType = assumeRulePool.get(target.method);
@@ -626,7 +626,7 @@
 
   private boolean canInsertForwardingMethod(DexClass holder, DexEncodedMethod target) {
     return appView.options().isGeneratingDex()
-        || ArrayUtils.contains(holder.interfaces.values, target.holder());
+        || ArrayUtils.contains(holder.interfaces.values, target.getHolderType());
   }
 
   private void markMatchingOverriddenMethods(
@@ -1109,7 +1109,7 @@
         if (options.isInterfaceMethodDesugaringEnabled()
             && encodedMethod.hasCode()
             && (encodedMethod.isPrivateMethod() || encodedMethod.isStaticMember())) {
-          DexClass holder = appView.definitionFor(encodedMethod.holder());
+          DexClass holder = appView.definitionFor(encodedMethod.getHolderType());
           if (holder != null && holder.isInterface()) {
             if (rule.isSpecific()) {
               options.reporter.warning(
@@ -1176,7 +1176,7 @@
     } else if (context instanceof ProguardAssumeNoSideEffectRule) {
       if (item.isDexEncodedMember()) {
         DexEncodedMember<?, ?> member = item.asDexEncodedMember();
-        if (member.holder() == appView.dexItemFactory().objectType) {
+        if (member.getHolderType() == appView.dexItemFactory().objectType) {
           assert member.isDexEncodedMethod();
           reportAssumeNoSideEffectsWarningForJavaLangClassMethod(
               member.asDexEncodedMethod(), (ProguardAssumeNoSideEffectRule) context);
@@ -1957,7 +1957,8 @@
             DexClass holder = appInfo.definitionForHolder(reference);
             DexEncodedField field = reference.lookupOnClass(holder);
             if (field != null
-                && (field.isStatic() || isKeptDirectlyOrIndirectly(field.holder(), appInfo))) {
+                && (field.isStatic()
+                    || isKeptDirectlyOrIndirectly(field.getHolderType(), appInfo))) {
               assert appInfo.isFieldRead(field)
                   : "Expected kept field `" + field.toSourceString() + "` to be read";
               assert appInfo.isFieldWritten(field)
@@ -1974,7 +1975,8 @@
                 : "Expected kept method `" + reference.toSourceString() + "` to be targeted";
             DexEncodedMethod method =
                 appInfo.definitionForHolder(reference).lookupMethod(reference);
-            if (!method.isAbstract() && isKeptDirectlyOrIndirectly(method.holder(), appInfo)) {
+            if (!method.isAbstract()
+                && isKeptDirectlyOrIndirectly(method.getHolderType(), appInfo)) {
               assert appInfo.isLiveMethod(reference)
                   : "Expected non-abstract kept method `"
                       + reference.toSourceString()
diff --git a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
index ddfea43..fb3868a 100644
--- a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
@@ -49,17 +49,24 @@
 
     @Override
     public void traceCheckCast(DexType type, ProgramMethod context) {
-      checkCastTypes.add(type.toBaseType(factory));
+      add(type, checkCastTypes);
     }
 
     @Override
     public void traceInstanceOf(DexType type, ProgramMethod context) {
-      instanceOfTypes.add(type.toBaseType(factory));
+      add(type, instanceOfTypes);
     }
 
     @Override
     public void traceExceptionGuard(DexType guard, ProgramMethod context) {
-      exceptionGuardTypes.add(guard);
+      add(guard, exceptionGuardTypes);
+    }
+
+    private void add(DexType type, Set<DexType> set) {
+      DexType baseType = type.toBaseType(factory);
+      if (baseType.isClassType()) {
+        set.add(baseType);
+      }
     }
 
     public void attach(Enqueuer enqueuer) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ScopedDexMethodSet.java b/src/main/java/com/android/tools/r8/shaking/ScopedDexMethodSet.java
index 4675107..d3b7e21 100644
--- a/src/main/java/com/android/tools/r8/shaking/ScopedDexMethodSet.java
+++ b/src/main/java/com/android/tools/r8/shaking/ScopedDexMethodSet.java
@@ -63,8 +63,8 @@
     }
     if (method.accessFlags.isMoreVisibleThan(
         existing.accessFlags,
-        method.holder().getPackageName(),
-        existing.holder().getPackageName())) {
+        method.getHolderType().getPackageName(),
+        existing.getHolderType().getPackageName())) {
       items.put(wrapped, method);
       return AddMethodIfMoreVisibleResult.ADDED_MORE_VISIBLE;
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index 97e980c..58e85ff 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.SingletonEquivalence;
 import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.BiMap;
@@ -199,7 +200,8 @@
   private final Map<MergeKey, Representative> representatives = new HashMap<>();
 
   private final BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
-  private final BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+  private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+      new BidirectionalOneToOneHashMap<>();
 
   private int numberOfMergedClasses = 0;
 
@@ -234,7 +236,8 @@
   private NestedGraphLens buildGraphLens() {
     if (!fieldMapping.isEmpty() || !methodMapping.isEmpty()) {
       BiMap<DexField, DexField> originalFieldSignatures = fieldMapping.inverse();
-      BiMap<DexMethod, DexMethod> originalMethodSignatures = methodMapping.inverse();
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+          methodMapping.getInverseOneToOneMap();
       return new NestedGraphLens(
           ImmutableMap.of(),
           methodMapping,
@@ -497,7 +500,7 @@
       newMethods.add(sourceMethodAfterMove);
 
       DexMethod originalMethod =
-          methodMapping.inverse().getOrDefault(sourceMethod.method, sourceMethod.method);
+          methodMapping.getRepresentativeKeyOrDefault(sourceMethod.method, sourceMethod.method);
       methodMapping.forcePut(originalMethod, sourceMethodAfterMove.method);
 
       existingMethods.add(equivalence.wrap(sourceMethodAfterMove.method));
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 c98e345..434b5d2 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -762,7 +762,7 @@
         // Conservatively find all possible targets for this method.
         LookupResultSuccess lookupResult =
             appInfo
-                .resolveMethodOnInterface(method.holder(), method.method)
+                .resolveMethodOnInterface(method.getHolderType(), method.method)
                 .lookupVirtualDispatchTargets(target, appInfo)
                 .asLookupResultSuccess();
         assert lookupResult != null;
@@ -1360,7 +1360,7 @@
     private DexEncodedMethod renameConstructor(
         DexEncodedMethod method, Predicate<DexMethod> availableMethodSignatures) {
       assert method.isInstanceInitializer();
-      DexType oldHolder = method.holder();
+      DexType oldHolder = method.getHolderType();
 
       DexMethod newSignature;
       int count = 1;
@@ -1396,7 +1396,7 @@
       // renamed already.
       assert !method.accessFlags.isConstructor() || strategy == Rename.NEVER;
       DexString oldName = method.method.name;
-      DexType oldHolder = method.holder();
+      DexType oldHolder = method.getHolderType();
 
       DexMethod newSignature;
       switch (strategy) {
@@ -1431,7 +1431,7 @@
     private DexEncodedField renameFieldIfNeeded(
         DexEncodedField field, Predicate<DexField> availableFieldSignatures) {
       DexString oldName = field.field.name;
-      DexType oldHolder = field.holder();
+      DexType oldHolder = field.getHolderType();
 
       DexField newSignature =
           application.dexItemFactory.createField(target.type, field.field.type, oldName);
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
index 2bee133..f634f48 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableSet;
@@ -74,7 +75,7 @@
       Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
           contextualVirtualToDirectMethodMaps,
       BiMap<DexField, DexField> originalFieldSignatures,
-      BiMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
       GraphLens previousLens) {
     super(
@@ -171,7 +172,8 @@
     private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
         contextualVirtualToDirectMethodMaps = new IdentityHashMap<>();
 
-    private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+    private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+        new BidirectionalOneToOneHashMap<>();
     private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges =
         new IdentityHashMap<>();
 
@@ -215,11 +217,12 @@
               context);
         }
       }
-      for (Map.Entry<DexMethod, DexMethod> entry : builder.originalMethodSignatures.entrySet()) {
-        newBuilder.recordMove(
-            entry.getValue(),
-            builder.getMethodSignatureAfterClassMerging(entry.getKey(), mergedClasses));
-      }
+      builder.originalMethodSignatures.forEach(
+          (renamedMethodSignature, originalMethodSignature) ->
+              newBuilder.recordMove(
+                  originalMethodSignature,
+                  builder.getMethodSignatureAfterClassMerging(
+                      renamedMethodSignature, mergedClasses)));
       for (Map.Entry<DexMethod, DexMethod> entry :
           builder.originalMethodSignaturesForBridges.entrySet()) {
         newBuilder.recordCreationOfBridgeMethod(
diff --git a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
index 8fd9edb..12a2462 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
@@ -7,10 +7,9 @@
 import com.android.tools.r8.Keep;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
-import com.android.tools.r8.tracereferences.Tracer.TracedClassImpl;
-import com.android.tools.r8.tracereferences.Tracer.TracedFieldImpl;
-import com.android.tools.r8.tracereferences.Tracer.TracedMethodImpl;
-import com.android.tools.r8.tracereferences.Tracer.TracedReferenceBase;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.FieldReference;
+import com.android.tools.r8.references.MethodReference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -18,14 +17,14 @@
 @Keep
 public class MissingDefinitionsDiagnostic implements Diagnostic {
 
-  private final Set<TracedClassImpl> missingClasses;
-  private final Set<TracedFieldImpl> missingFields;
-  private final Set<TracedMethodImpl> missingMethods;
+  private final Set<ClassReference> missingClasses;
+  private final Set<FieldReference> missingFields;
+  private final Set<MethodReference> missingMethods;
 
   MissingDefinitionsDiagnostic(
-      Set<TracedClassImpl> missingClasses,
-      Set<TracedFieldImpl> missingFields,
-      Set<TracedMethodImpl> missingMethods) {
+      Set<ClassReference> missingClasses,
+      Set<FieldReference> missingFields,
+      Set<MethodReference> missingMethods) {
     this.missingClasses = missingClasses;
     this.missingFields = missingFields;
     this.missingMethods = missingMethods;
@@ -41,15 +40,25 @@
     return Position.UNKNOWN;
   }
 
-  private <T extends TracedReferenceBase<?, ?>> void appendSorted(
-      StringBuilder builder, Set<T> missing) {
+  private <T> void appendSorted(StringBuilder builder, Set<T> missing) {
     missing.stream()
-        .map(element -> element.getReference())
         .map(Object::toString)
         .sorted()
         .forEach(item -> builder.append("  ").append(item).append(System.lineSeparator()));
   }
 
+  public Set<ClassReference> getMissingClasses() {
+    return missingClasses;
+  }
+
+  public Set<FieldReference> getMissingFields() {
+    return missingFields;
+  }
+
+  public Set<MethodReference> getMissingMethods() {
+    return missingMethods;
+  }
+
   @Override
   public String getDiagnosticMessage() {
     StringBuilder builder = new StringBuilder("Tracereferences found ");
diff --git a/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java b/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java
deleted file mode 100644
index a29fdb5..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/PrintUsesFormatter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.tracereferences;
-
-import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedClass;
-import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedField;
-import com.android.tools.r8.tracereferences.TraceReferencesConsumer.TracedMethod;
-import java.util.List;
-
-class PrintUsesFormatter extends Formatter {
-
-  @Override
-  protected void printConstructorName(MethodReference method) {
-    if (method.getMethodName().equals("<clinit>")) {
-      append("<clinit>");
-    } else {
-      String holderName = method.getHolderClass().getTypeName();
-      String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
-      append(constructorName);
-    }
-  }
-
-  @Override
-  protected void printMethod(TracedMethod method) {
-    append(method.getReference().getHolderClass().getTypeName() + ": ");
-    printNameAndReturn(method.getReference());
-    printArguments(method.getReference());
-    appendLine("");
-  }
-
-  @Override
-  protected void printPackageNames(List<String> packageNames) {
-    // No need to print package names for text output.
-  }
-
-  @Override
-  protected void printTypeHeader(TracedClass type) {
-    appendLine(type.getReference().getTypeName());
-  }
-
-  @Override
-  protected void printTypeFooter() {}
-
-  @Override
-  protected void printField(TracedField field) {
-    appendLine(
-        field.getReference().getHolderClass().getTypeName()
-            + ": "
-            + field.getReference().getFieldType().getTypeName()
-            + " "
-            + field.getReference().getFieldName());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
index 3a3faeb..96956b0 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
@@ -16,7 +16,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -37,8 +36,6 @@
                   "Usage: tracereferences <command> [<options>] [@<argfile>]",
                   " Where <command> is one of:",
                   "  --check                 # Run emitting only diagnostics messages.",
-                  "  --print-usage           # Traced references will be output in the print-usage",
-                  "                          # format.",
                   "  --keep-rules [<keep-rules-options>]",
                   "                          # Traced references will be output in the keep-rules",
                   "                          # format.",
@@ -92,7 +89,6 @@
 
   private enum Command {
     CHECK,
-    PRINTUSAGE,
     KEEP_RULES;
   }
 
@@ -137,9 +133,6 @@
       } else if (arg.equals("--check")) {
         checkCommandNotSet(command, builder, origin);
         command = Command.CHECK;
-      } else if (arg.equals("--print-usage")) {
-        checkCommandNotSet(command, builder, origin);
-        command = Command.PRINTUSAGE;
       } else if (arg.equals("--keep-rules")) {
         checkCommandNotSet(command, builder, origin);
         command = Command.KEEP_RULES;
@@ -174,15 +167,13 @@
     if (command == null) {
       builder.error(
           new StringDiagnostic(
-              "Missing command, specify one of 'check', '--print-usage' or '--keep-rules'",
-              origin));
+              "Missing command, specify one of 'check' or '--keep-rules'", origin));
       return builder;
     }
 
     if (command == Command.CHECK && output != null) {
       builder.error(
-          new StringDiagnostic(
-              "Using '--output' requires command '--print-usage' or '--keep-rules'", origin));
+          new StringDiagnostic("Using '--output' requires command '--keep-rules'", origin));
       return builder;
     }
 
@@ -207,24 +198,6 @@
                         : new WriterConsumer(null, new PrintWriter(System.out)))
                 .build());
         break;
-      case PRINTUSAGE:
-        final Path finalOutput = output;
-        builder.setConsumer(
-            new TraceReferencesPrintUsage() {
-              @Override
-              public void finished(DiagnosticsHandler handler) {
-                PrintStream out = System.out;
-                if (finalOutput != null) {
-                  try {
-                    out = new PrintStream(Files.newOutputStream(finalOutput));
-                  } catch (IOException e) {
-                    handler.error(new ExceptionDiagnostic(e));
-                  }
-                }
-                out.print(get());
-              }
-            });
-        break;
       default:
         throw new Unreachable();
     }
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java
deleted file mode 100644
index 80e8096..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.tracereferences;
-
-import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.references.PackageReference;
-
-class TraceReferencesPrintUsage implements TraceReferencesConsumer {
-
-  private final TraceReferencesResult.Builder traceReferencesResultBuilder =
-      TraceReferencesResult.builder();
-  private boolean finishedCalled = false;
-
-  @Override
-  public void acceptType(TracedClass type, DiagnosticsHandler handler) {
-    assert !finishedCalled;
-    traceReferencesResultBuilder.acceptType(type, handler);
-  }
-
-  @Override
-  public void acceptField(TracedField field, DiagnosticsHandler handler) {
-    assert !finishedCalled;
-    traceReferencesResultBuilder.acceptField(field, handler);
-  }
-
-  @Override
-  public void acceptMethod(TracedMethod method, DiagnosticsHandler handler) {
-    assert !finishedCalled;
-    traceReferencesResultBuilder.acceptMethod(method, handler);
-  }
-
-  @Override
-  public void acceptPackage(PackageReference pkg, DiagnosticsHandler handler) {
-    assert !finishedCalled;
-    traceReferencesResultBuilder.acceptPackage(pkg, handler);
-  }
-
-  @Override
-  public void finished(DiagnosticsHandler handler) {
-    assert !finishedCalled;
-    finishedCalled = true;
-  }
-
-  public String get() {
-    Formatter formatter = new PrintUsesFormatter();
-    formatter.format(traceReferencesResultBuilder.build());
-    return formatter.get();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 8b571a8..ddd3f78 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -259,9 +259,9 @@
     private final TraceReferencesConsumer consumer;
     private DexProgramClass context;
     private final DiagnosticsHandler diagnostics;
-    private final Set<TracedClassImpl> missingClasses = new HashSet<>();
-    private final Set<TracedFieldImpl> missingFields = new HashSet<>();
-    private final Set<TracedMethodImpl> missingMethods = new HashSet<>();
+    private final Set<ClassReference> missingClasses = new HashSet<>();
+    private final Set<FieldReference> missingFields = new HashSet<>();
+    private final Set<MethodReference> missingMethods = new HashSet<>();
 
     UseCollector(
         DexItemFactory factory, TraceReferencesConsumer consumer, DiagnosticsHandler diagnostics) {
@@ -299,7 +299,7 @@
     private void addField(DexField field) {
       addType(field.type);
       DexEncodedField baseField = appInfo.resolveField(field).getResolvedField();
-      if (baseField != null && baseField.holder() != field.holder) {
+      if (baseField != null && baseField.getHolderType() != field.holder) {
         field = baseField.field;
       }
       addType(field.holder);
@@ -310,7 +310,7 @@
         if (!tracedField.isMissingDefinition()
             && baseField.accessFlags.isVisibilityDependingOnPackage()) {
           consumer.acceptPackage(
-              Reference.packageFromString(baseField.holder().getPackageName()), diagnostics);
+              Reference.packageFromString(baseField.getHolderType().getPackageName()), diagnostics);
         }
       }
     }
@@ -330,7 +330,8 @@
         if (!tracedMethod.isMissingDefinition()
             && definition.accessFlags.isVisibilityDependingOnPackage()) {
           consumer.acceptPackage(
-              Reference.packageFromString(definition.holder().getPackageName()), diagnostics);
+              Reference.packageFromString(definition.getHolderType().getPackageName()),
+              diagnostics);
         }
       }
     }
@@ -347,10 +348,10 @@
       collectMissing(tracedMethod, missingMethods);
     }
 
-    private <T extends TracedReferenceBase<?, ?>> void collectMissing(
-        T tracedReference, Set<T> missingCollection) {
+    private <R, T extends TracedReferenceBase<R, ?>> void collectMissing(
+        T tracedReference, Set<R> missingCollection) {
       if (tracedReference.isMissingDefinition()) {
-        missingCollection.add(tracedReference);
+        missingCollection.add(tracedReference.getReference());
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 4478209..21c3447 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isAarFile;
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
@@ -504,7 +505,9 @@
             mainDexList.add(mainDexListResource.getString());
           }
         }
-        mainDexList.addAll(getMainDexClasses());
+        for (String mainDexClass : getMainDexClasses()) {
+          mainDexList.add(mainDexClass.replace(".", "/") + CLASS_EXTENSION);
+        }
         String join = StringUtils.join(mainDexList, "\n");
         writeToZipStream(out, dumpMainDexListResourceFileName, join.getBytes(), ZipEntry.DEFLATED);
       }
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 9116bb5..f4cdbaa 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -238,6 +238,9 @@
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
   public boolean enableStaticClassMerging = true;
   public boolean enableHorizontalClassMerging = true;
+  public boolean enableHorizontalClassMergingConstructorMerging = true;
+  public int horizontalClassMergingMaxGroupSize = 30;
+  public int horizontalClassMergingSyntheticArgumentCount = 3;
   public boolean enableHorizontalClassMergingOfKotlinLambdas = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableArgumentRemoval = true;
@@ -1183,8 +1186,11 @@
     // b/172508621
     public boolean sortMethodsOnCfOutput =
         System.getProperty("com.android.tools.r8.sortMethodsOnCfWriting") != null;
-    public boolean allowDesugaredInput =
-        System.getProperty("com.android.tools.r8.allowDesugaredInput") != null;
+    // Desugaring is not fully idempotent. With this option turned on all desugared input is
+    // allowed, and if it is detected that the desugared input cannot be reprocessed, that input
+    // will be passed-through without the problematic rewritings applied.
+    public boolean allowAllDesugaredInput =
+        System.getProperty("com.android.tools.r8.allowAllDesugaredInput") != null;
   }
 
   public static class CallSiteOptimizationOptions {
diff --git a/src/main/java/com/android/tools/r8/utils/IterableUtils.java b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
index 57cf52b..aee34f6 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -9,6 +9,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -65,11 +66,32 @@
     return Iterables.concat(singleton(t), iterable);
   }
 
+  public static <T> T flatten(T init, BiFunction<T, T, T> combine, Iterable<? extends T> iterable) {
+    T v = init;
+    for (T t : iterable) {
+      v = combine.apply(v, t);
+    }
+    return v;
+  }
+
+  public static int sumInt(Iterable<Integer> iterable) {
+    return flatten(0, Integer::sum, iterable);
+  }
+
+  public static <F> int sumInt(Iterable<F> iterable, Function<? super F, Integer> fn) {
+    Iterable<Integer> integers = Iterables.transform(iterable, i -> fn.apply(i));
+    return sumInt(integers);
+  }
+
   public static <T, U> Iterable<U> flatMap(
       Iterable<T> iterable, Function<? super T, Iterable<U>> map) {
     return Iterables.concat(Iterables.transform(iterable, val -> map.apply(val)));
   }
 
+  public static <T> Iterable<T> empty() {
+    return Collections.emptyList();
+  }
+
   public static <T> Iterable<T> emptyIf(Iterable<T> iterable, boolean condition) {
     if (condition) {
       return Collections.emptySet();
diff --git a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
index b12de07..35f950a 100644
--- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -87,6 +87,27 @@
     throw new Unreachable();
   }
 
+  public static <T> T previousUntilUnsafe(ListIterator<T> iterator, Predicate<T> predicate) {
+    while (iterator.hasPrevious()) {
+      T previous = iterator.previous();
+      if (predicate.test(previous)) {
+        return previous;
+      }
+    }
+    return null;
+  }
+
+  public static <T> T removeFirst(Iterator<T> iterator, Predicate<T> predicate) {
+    while (iterator.hasNext()) {
+      T item = iterator.next();
+      if (predicate.test(item)) {
+        iterator.remove();
+        return item;
+      }
+    }
+    return null;
+  }
+
   public static <T> void removeIf(Iterator<T> iterator, Predicate<T> predicate) {
     while (iterator.hasNext()) {
       T item = iterator.next();
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 51a3778..b1099e1 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -187,7 +187,7 @@
       if (parsedData != null || parsedKotlinSourceDebugExtensions.containsKey(holder)) {
         return parsedData;
       }
-      DexClass clazz = appView.definitionFor(currentMethod.holder());
+      DexClass clazz = appView.definitionFor(currentMethod.getHolderType());
       DexValueString dexValueString = appView.getSourceDebugExtensionForType(clazz);
       if (dexValueString != null) {
         parsedData = KotlinSourceDebugExtensionParser.parse(dexValueString.value.toString());
@@ -475,7 +475,7 @@
         continue;
       }
       // We use the same name for interface names even if it has different types.
-      DexProgramClass clazz = appView.definitionForProgramType(method.holder());
+      DexProgramClass clazz = appView.definitionForProgramType(method.getHolderType());
       DexClassAndMethod lookupResult =
           appView.appInfo().lookupMaximallySpecificMethod(clazz, method.method);
       if (lookupResult == null) {
@@ -808,7 +808,7 @@
     }
     method.setCode(
         new CfCode(
-            method.holder(),
+            method.getHolderType(),
             oldCode.getMaxStack(),
             oldCode.getMaxLocals(),
             newInstructions,
diff --git a/src/main/java/com/android/tools/r8/utils/MapUtils.java b/src/main/java/com/android/tools/r8/utils/MapUtils.java
index c899a75..ac79586 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -12,6 +12,14 @@
 
 public class MapUtils {
 
+  public static <K, V> K firstKey(Map<K, V> map) {
+    return map.keySet().iterator().next();
+  }
+
+  public static <K, V> V firstValue(Map<K, V> map) {
+    return map.values().iterator().next();
+  }
+
   public static <K, V> Map<K, V> map(
       Map<K, V> map,
       IntFunction<Map<K, V>> factory,
diff --git a/src/main/java/com/android/tools/r8/utils/OptionalBool.java b/src/main/java/com/android/tools/r8/utils/OptionalBool.java
index abb048d..4443355 100644
--- a/src/main/java/com/android/tools/r8/utils/OptionalBool.java
+++ b/src/main/java/com/android/tools/r8/utils/OptionalBool.java
@@ -15,6 +15,11 @@
         }
 
         @Override
+        public int ordinal() {
+          return 1;
+        }
+
+        @Override
         public String toString() {
           return "true";
         }
@@ -29,6 +34,11 @@
         }
 
         @Override
+        public int ordinal() {
+          return 0;
+        }
+
+        @Override
         public String toString() {
           return "false";
         }
@@ -43,6 +53,11 @@
         }
 
         @Override
+        public int ordinal() {
+          return 2;
+        }
+
+        @Override
         public String toString() {
           return "unknown";
         }
@@ -73,6 +88,8 @@
     return System.identityHashCode(this);
   }
 
+  public abstract int ordinal();
+
   // Force all subtypes to implement toString().
   @Override
   public abstract String toString();
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
new file mode 100644
index 0000000..a4878dc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import java.util.Map;
+
+public abstract class BidirectionalManyToManyRepresentativeMap<K, V> {
+
+  public static <K, V> BidirectionalManyToManyRepresentativeMap<K, V> empty() {
+    return new EmptyBidirectionalManyToManyRepresentativeMap<>();
+  }
+
+  public abstract boolean containsKey(K key);
+
+  public abstract boolean containsValue(V value);
+
+  public abstract Map<K, V> getForwardBacking();
+
+  public abstract Map<V, K> getInverseBacking();
+
+  public final Inverse getInverseManyToManyMap() {
+    return new Inverse();
+  }
+
+  public abstract K getRepresentativeKey(V value);
+
+  public final K getRepresentativeKeyOrDefault(V value, K defaultValue) {
+    K representativeKey = getRepresentativeKey(value);
+    return representativeKey != null ? representativeKey : defaultValue;
+  }
+
+  public abstract V getRepresentativeValue(K key);
+
+  public final V getRepresentativeValueOrDefault(K key, V defaultValue) {
+    V representativeValue = getRepresentativeValue(key);
+    return representativeValue != null ? representativeValue : defaultValue;
+  }
+
+  public abstract Iterable<K> getKeys(V value);
+
+  public abstract Iterable<V> getValues(K key);
+
+  public abstract boolean isEmpty();
+
+  public class Inverse extends BidirectionalManyToManyRepresentativeMap<V, K> {
+
+    @Override
+    public boolean containsKey(V key) {
+      return BidirectionalManyToManyRepresentativeMap.this.containsValue(key);
+    }
+
+    @Override
+    public boolean containsValue(K value) {
+      return BidirectionalManyToManyRepresentativeMap.this.containsKey(value);
+    }
+
+    @Override
+    public Map<V, K> getForwardBacking() {
+      return BidirectionalManyToManyRepresentativeMap.this.getInverseBacking();
+    }
+
+    @Override
+    public Map<K, V> getInverseBacking() {
+      return BidirectionalManyToManyRepresentativeMap.this.getForwardBacking();
+    }
+
+    @Override
+    public V getRepresentativeKey(K value) {
+      return BidirectionalManyToManyRepresentativeMap.this.getRepresentativeValue(value);
+    }
+
+    @Override
+    public K getRepresentativeValue(V key) {
+      return BidirectionalManyToManyRepresentativeMap.this.getRepresentativeKey(key);
+    }
+
+    @Override
+    public Iterable<V> getKeys(K value) {
+      return BidirectionalManyToManyRepresentativeMap.this.getValues(value);
+    }
+
+    @Override
+    public Iterable<K> getValues(V key) {
+      return BidirectionalManyToManyRepresentativeMap.this.getKeys(key);
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return BidirectionalManyToManyRepresentativeMap.this.isEmpty();
+    }
+  }
+}
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
new file mode 100644
index 0000000..d618b52
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
@@ -0,0 +1,134 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import com.android.tools.r8.utils.IterableUtils;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class BidirectionalOneToOneHashMap<K, V>
+    extends BidirectionalManyToManyRepresentativeMap<K, V> implements Map<K, V> {
+
+  private final BiMap<K, V> backing;
+
+  public BidirectionalOneToOneHashMap() {
+    this(HashBiMap.create());
+  }
+
+  public BidirectionalOneToOneHashMap(BiMap<K, V> backing) {
+    this.backing = backing;
+  }
+
+  @Override
+  public void clear() {
+    backing.clear();
+  }
+
+  @Override
+  public boolean containsKey(Object key) {
+    return backing.containsKey(key);
+  }
+
+  @Override
+  public boolean containsValue(Object value) {
+    return backing.containsValue(value);
+  }
+
+  @Override
+  public Set<Entry<K, V>> entrySet() {
+    return backing.entrySet();
+  }
+
+  public V forcePut(K key, V value) {
+    return backing.forcePut(key, value);
+  }
+
+  @Override
+  public V get(Object key) {
+    return backing.get(key);
+  }
+
+  @Override
+  public BiMap<K, V> getForwardBacking() {
+    return backing;
+  }
+
+  @Override
+  public BiMap<V, K> getInverseBacking() {
+    return backing.inverse();
+  }
+
+  public BidirectionalOneToOneHashMap<V, K> getInverseOneToOneMap() {
+    return new BidirectionalOneToOneHashMap<>(backing.inverse());
+  }
+
+  @Override
+  public K getRepresentativeKey(V value) {
+    return backing.inverse().get(value);
+  }
+
+  @Override
+  public V getRepresentativeValue(K key) {
+    return backing.get(key);
+  }
+
+  @Override
+  public Iterable<K> getKeys(V value) {
+    if (containsValue(value)) {
+      return IterableUtils.singleton(getRepresentativeKey(value));
+    }
+    return IterableUtils.empty();
+  }
+
+  @Override
+  public Iterable<V> getValues(K key) {
+    if (containsKey(key)) {
+      return IterableUtils.singleton(getRepresentativeValue(key));
+    }
+    return IterableUtils.empty();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  @Override
+  public Set<K> keySet() {
+    return backing.keySet();
+  }
+
+  @Override
+  public V put(K key, V value) {
+    return backing.put(key, value);
+  }
+
+  public void putAll(BidirectionalOneToOneHashMap<K, V> map) {
+    putAll(map.backing);
+  }
+
+  @Override
+  public void putAll(Map<? extends K, ? extends V> map) {
+    backing.putAll(map);
+  }
+
+  @Override
+  public V remove(Object key) {
+    return backing.remove(key);
+  }
+
+  @Override
+  public int size() {
+    return backing.size();
+  }
+
+  @Override
+  public Collection<V> values() {
+    return backing.values();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java
new file mode 100644
index 0000000..dbcb753
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import com.android.tools.r8.utils.IterableUtils;
+import java.util.Collections;
+import java.util.Map;
+
+public class EmptyBidirectionalManyToManyRepresentativeMap<K, V>
+    extends BidirectionalManyToManyRepresentativeMap<K, V> {
+
+  @Override
+  public boolean containsKey(K key) {
+    return false;
+  }
+
+  @Override
+  public boolean containsValue(V value) {
+    return false;
+  }
+
+  @Override
+  public Map<K, V> getForwardBacking() {
+    return Collections.emptyMap();
+  }
+
+  @Override
+  public Map<V, K> getInverseBacking() {
+    return Collections.emptyMap();
+  }
+
+  @Override
+  public K getRepresentativeKey(V value) {
+    return null;
+  }
+
+  @Override
+  public V getRepresentativeValue(K key) {
+    return null;
+  }
+
+  @Override
+  public Iterable<K> getKeys(V value) {
+    return IterableUtils.empty();
+  }
+
+  @Override
+  public Iterable<V> getValues(K key) {
+    return IterableUtils.empty();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
index 832d172..32221a0 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
@@ -5,10 +5,14 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+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.DexTypeList;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Comparator;
+import java.util.Iterator;
 
 /** Base class for a visitor implementing compareTo on a structural item. */
 public abstract class CompareToVisitor {
@@ -17,13 +21,29 @@
 
   public abstract void visitInt(int value1, int value2);
 
-  public abstract void visitDexString(
-      DexString string1, DexString string2, Comparator<DexString> comparator);
+  public abstract void visitLong(long value1, long value2);
+
+  public abstract void visitFloat(float value1, float value2);
+
+  public abstract void visitDouble(double value1, double value2);
+
+  /** Base for visiting an enumeration of items. */
+  public abstract <S> void visitItemIterator(
+      Iterator<S> it1, Iterator<S> it2, CompareToAccept<S> compareToAccept);
+
+  public final <S extends StructuralItem<S>> void visitItemArray(S[] items1, S[] items2) {
+    visitItemCollection(Arrays.asList(items1), Arrays.asList(items2));
+  }
+
+  public final <S extends StructuralItem<S>> void visitItemCollection(
+      Collection<S> items1, Collection<S> items2) {
+    visitItemIterator(items1.iterator(), items2.iterator(), S::acceptCompareTo);
+  }
+
+  public abstract void visitDexString(DexString string1, DexString string2);
 
   public abstract void visitDexType(DexType type1, DexType type2);
 
-  public abstract void visitDexTypeList(DexTypeList types1, DexTypeList types2);
-
   public void visitDexField(DexField field1, DexField field2) {
     visit(field1, field2, field1.getStructuralAccept());
   }
@@ -32,6 +52,8 @@
     visit(method1, method2, method1.getStructuralAccept());
   }
 
+  public abstract void visitDexReference(DexReference reference1, DexReference reference2);
+
   public abstract <S> void visit(S item1, S item2, StructuralAccept<S> accept);
 
   @Deprecated
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
index b7d016a..1f517d0 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
@@ -3,14 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
 import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
 
 /** Base class to share most visiting methods */
 public abstract class CompareToVisitorBase extends CompareToVisitor {
@@ -44,23 +47,57 @@
   }
 
   @Override
-  public final void visitDexString(
-      DexString string1, DexString string2, Comparator<DexString> comparator) {
+  public void visitLong(long value1, long value2) {
     if (stillEqual()) {
-      setOrder(comparator.compare(string1, string2));
+      setOrder(Long.compare(value1, value2));
     }
   }
 
   @Override
-  public final void visitDexTypeList(DexTypeList types1, DexTypeList types2) {
-    // Comparison is lexicographic with comparisons between items prior to the length of the lists.
+  public void visitFloat(float value1, float value2) {
     if (stillEqual()) {
-      int length = Math.min(types1.size(), types2.size());
-      for (int i = 0; i < length && stillEqual(); i++) {
-        visitDexType(types1.values[i], types2.values[i]);
-      }
+      setOrder(Float.compare(value1, value2));
+    }
+  }
+
+  @Override
+  public void visitDouble(double value1, double value2) {
+    if (stillEqual()) {
+      setOrder(Double.compare(value1, value2));
+    }
+  }
+
+  @Override
+  public <S> void visitItemIterator(
+      Iterator<S> it1, Iterator<S> it2, CompareToAccept<S> compareToAccept) {
+    while (stillEqual() && it1.hasNext() && it2.hasNext()) {
+      compareToAccept.acceptCompareTo(it1.next(), it2.next(), this);
+    }
+    if (stillEqual()) {
+      visitBool(it1.hasNext(), it2.hasNext());
+    }
+  }
+
+  @Override
+  public void visitDexString(DexString string1, DexString string2) {
+    if (stillEqual()) {
+      setOrder(string1.compareTo(string2));
+    }
+  }
+
+  @Override
+  public void visitDexReference(DexReference reference1, DexReference reference2) {
+    if (stillEqual()) {
+      visitInt(reference1.referenceTypeOrder(), reference2.referenceTypeOrder());
       if (stillEqual()) {
-        visitInt(types1.size(), types2.size());
+        assert reference1.getClass() == reference2.getClass();
+        if (reference1.isDexType()) {
+          visitDexType(reference1.asDexType(), reference2.asDexType());
+        } else if (reference1.isDexField()) {
+          visitDexField(reference1.asDexField(), reference2.asDexField());
+        } else {
+          visitDexMethod(reference1.asDexMethod(), reference2.asDexMethod());
+        }
       }
     }
   }
@@ -75,7 +112,7 @@
   @Override
   public final <S> void visit(S item1, S item2, StructuralAccept<S> accept) {
     if (stillEqual()) {
-      accept.accept(new ItemSpecification<>(item1, item2, this));
+      accept.apply(new ItemSpecification<>(item1, item2, this));
     }
   }
 
@@ -101,13 +138,49 @@
 
     @Override
     public ItemSpecification<T> withBool(Predicate<T> getter) {
-      parent.visitBool(getter.test(item1), getter.test(item2));
+      if (parent.stillEqual()) {
+        parent.visitBool(getter.test(item1), getter.test(item2));
+      }
       return this;
     }
 
     @Override
     public ItemSpecification<T> withInt(ToIntFunction<T> getter) {
-      parent.visitInt(getter.applyAsInt(item1), getter.applyAsInt(item2));
+      if (parent.stillEqual()) {
+        parent.visitInt(getter.applyAsInt(item1), getter.applyAsInt(item2));
+      }
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withLong(ToLongFunction<T> getter) {
+      if (parent.stillEqual()) {
+        parent.visitLong(getter.applyAsLong(item1), getter.applyAsLong(item2));
+      }
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withDouble(ToDoubleFunction<T> getter) {
+      if (parent.stillEqual()) {
+        parent.visitDouble(getter.applyAsDouble(item1), getter.applyAsDouble(item2));
+      }
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withIntArray(Function<T, int[]> getter) {
+      if (parent.stillEqual()) {
+        int[] is1 = getter.apply(item1);
+        int[] is2 = getter.apply(item2);
+        int minLength = Math.min(is1.length, is2.length);
+        for (int i = 0; i < minLength && parent.stillEqual(); i++) {
+          parent.visitInt(is1[i], is2[i]);
+        }
+        if (parent.stillEqual()) {
+          parent.visitInt(is1.length, is2.length);
+        }
+      }
       return this;
     }
 
@@ -121,12 +194,21 @@
         boolean test1 = predicate.test(item1);
         boolean test2 = predicate.test(item2);
         if (test1 && test2) {
-          compare.accept(getter.apply(item1), getter.apply(item2), parent);
+          compare.acceptCompareTo(getter.apply(item1), getter.apply(item2), parent);
         } else {
           parent.visitBool(test1, test2);
         }
       }
       return this;
     }
+
+    @Override
+    protected <S> ItemSpecification<T> withItemIterator(
+        Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+      if (parent.stillEqual()) {
+        parent.visitItemIterator(getter.apply(item1), getter.apply(item2), compare);
+      }
+      return this;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
index a16575d..5295766 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
@@ -18,7 +18,7 @@
   public static <T> int run(
       T item1, T item2, NamingLens namingLens, CompareToAccept<T> compareToAccept) {
     CompareToVisitorWithNamingLens state = new CompareToVisitorWithNamingLens(namingLens);
-    compareToAccept.accept(item1, item2, state);
+    compareToAccept.acceptCompareTo(item1, item2, state);
     return state.getOrder();
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithStringTable.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithStringTable.java
new file mode 100644
index 0000000..dfd9fa0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithStringTable.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.NamingLensComparable;
+import com.android.tools.r8.naming.NamingLens;
+import java.util.function.ToIntFunction;
+
+public class CompareToVisitorWithStringTable extends CompareToVisitorWithNamingLens {
+
+  public static <T extends NamingLensComparable<T>> int run(
+      T item1, T item2, NamingLens namingLens, ToIntFunction<DexString> stringTable) {
+    CompareToVisitorWithNamingLens state =
+        new CompareToVisitorWithStringTable(namingLens, stringTable);
+    item1.acceptCompareTo(item2, state);
+    return state.getOrder();
+  }
+
+  private final ToIntFunction<DexString> stringTable;
+
+  public CompareToVisitorWithStringTable(
+      NamingLens namingLens, ToIntFunction<DexString> stringTable) {
+    super(namingLens);
+    this.stringTable = stringTable;
+  }
+
+  @Override
+  public void visitDexString(DexString string1, DexString string2) {
+    if (stillEqual()) {
+      visitInt(stringTable.applyAsInt(string1), stringTable.applyAsInt(string2));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
index f77ab53..62b1942 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
@@ -15,7 +15,7 @@
   public static <T> int run(
       T item1, T item2, RepresentativeMap map, CompareToAccept<T> compareToAccept) {
     CompareToVisitorWithTypeEquivalence state = new CompareToVisitorWithTypeEquivalence(map);
-    compareToAccept.accept(item1, item2, state);
+    compareToAccept.acceptCompareTo(item1, item2, state);
     return state.getOrder();
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeTable.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeTable.java
new file mode 100644
index 0000000..94fb1f9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeTable.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.NamingLensComparable;
+import com.android.tools.r8.naming.NamingLens;
+import java.util.function.ToIntFunction;
+
+public class CompareToVisitorWithTypeTable extends CompareToVisitorWithStringTable {
+
+  public static <T extends NamingLensComparable<T>> int run(
+      T item1,
+      T item2,
+      NamingLens namingLens,
+      ToIntFunction<DexString> stringTable,
+      ToIntFunction<DexType> typeTable) {
+    CompareToVisitorWithNamingLens state =
+        new CompareToVisitorWithTypeTable(namingLens, stringTable, typeTable);
+    item1.acceptCompareTo(item2, state);
+    return state.getOrder();
+  }
+
+  private final ToIntFunction<DexType> typeTable;
+
+  public CompareToVisitorWithTypeTable(
+      NamingLens namingLens,
+      ToIntFunction<DexString> stringTable,
+      ToIntFunction<DexType> typeTable) {
+    super(namingLens, stringTable);
+    this.typeTable = typeTable;
+  }
+
+  @Override
+  public void visitDexType(DexType type1, DexType type2) {
+    if (stillEqual()) {
+      visitInt(typeTable.applyAsInt(type1), typeTable.applyAsInt(type2));
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
index ab75fb4..1b5b400 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -5,9 +5,13 @@
 
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.Arrays;
+import java.util.Iterator;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
 import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
 
 /**
  * Simple hash code implementation.
@@ -21,7 +25,7 @@
 
   public static <T> int run(T item, StructuralAccept<T> visit) {
     HashCodeVisitor<T> visitor = new HashCodeVisitor<>(item);
-    visit.accept(visitor);
+    visit.apply(visitor);
     return visitor.hashCode;
   }
 
@@ -56,6 +60,21 @@
   }
 
   @Override
+  public HashCodeVisitor<T> withLong(ToLongFunction<T> getter) {
+    return amend(Long.hashCode(getter.applyAsLong(item)));
+  }
+
+  @Override
+  public HashCodeVisitor<T> withDouble(ToDoubleFunction<T> getter) {
+    return amend(Double.hashCode(getter.applyAsDouble(item)));
+  }
+
+  @Override
+  public HashCodeVisitor<T> withIntArray(Function<T, int[]> getter) {
+    return amend(Arrays.hashCode(getter.apply(item)));
+  }
+
+  @Override
   protected <S> HashCodeVisitor<T> withConditionalCustomItem(
       Predicate<T> predicate,
       Function<T, S> getter,
@@ -64,9 +83,19 @@
     if (predicate.test(item)) {
       return amend(getter.apply(item).hashCode());
     } else {
-      // Use the value 1 for the failing-predicate case such that a different hash is obtained for
+      // Use the value 1 for the failing-predicate case such that a different hash is obtained for,
       // eg, {null, null} and {null}.
       return amend(1);
     }
   }
+
+  @Override
+  protected <S> HashCodeVisitor<T> withItemIterator(
+      Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+    Iterator<S> it = getter.apply(item);
+    while (it.hasNext()) {
+      amend(it.next().hashCode());
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
index bfd5e0d..ed0f61e 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
@@ -3,10 +3,16 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+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.DexTypeList;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
 import com.google.common.hash.Hasher;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.function.BiConsumer;
 
 public abstract class HashingVisitor {
@@ -15,11 +21,38 @@
 
   public abstract void visitInt(int value);
 
+  public abstract void visitFloat(float value);
+
+  public abstract void visitLong(long value);
+
+  public abstract void visitDouble(double value);
+
+  /** Base for visiting an enumeration of items. */
+  protected abstract <S> void visitItemIterator(Iterator<S> it, HashingAccept<S> hashingAccept);
+
+  public final <S extends StructuralItem<S>> void visitItemArray(S[] items) {
+    visitItemCollection(Arrays.asList(items));
+  }
+
+  public final <S extends StructuralItem<S>> void visitItemCollection(Collection<S> items) {
+    visitItemIterator(items.iterator(), S::acceptHashing);
+  }
+
   public abstract void visitDexString(DexString string);
 
   public abstract void visitDexType(DexType type);
 
-  public abstract void visitDexTypeList(DexTypeList types);
+  public void visitDexField(DexField field) {
+    visit(field, field.getStructuralAccept());
+  }
+
+  public void visitDexMethod(DexMethod method) {
+    visit(method, method.getStructuralAccept());
+  }
+
+  public void visitDexReference(DexReference reference) {
+    reference.accept(this::visitDexType, this::visitDexField, this::visitDexMethod);
+  }
 
   public abstract <S> void visit(S item, StructuralAccept<S> accept);
 
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
index 2036b1b..63989f5 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
@@ -5,14 +5,16 @@
 
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
 import com.google.common.hash.Hasher;
+import java.util.Iterator;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
 import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
 
 /** Visitor for hashing a structural item under some assumed type equivalence. */
 public class HashingVisitorWithTypeEquivalence extends HashingVisitor {
@@ -24,7 +26,7 @@
 
   public static <T> void run(
       T item, Hasher hasher, RepresentativeMap map, HashingAccept<T> hashingAccept) {
-    hashingAccept.accept(item, new HashingVisitorWithTypeEquivalence(hasher, map));
+    hashingAccept.acceptHashing(item, new HashingVisitorWithTypeEquivalence(hasher, map));
   }
 
   private final Hasher hash;
@@ -46,6 +48,21 @@
   }
 
   @Override
+  public void visitFloat(float value) {
+    hash.putFloat(value);
+  }
+
+  @Override
+  public void visitLong(long value) {
+    hash.putLong(value);
+  }
+
+  @Override
+  public void visitDouble(double value) {
+    hash.putDouble(value);
+  }
+
+  @Override
   public void visitDexString(DexString string) {
     visitInt(string.hashCode());
   }
@@ -56,13 +73,15 @@
   }
 
   @Override
-  public void visitDexTypeList(DexTypeList types) {
-    types.forEach(this::visitDexType);
+  public <S> void visit(S item, StructuralAccept<S> accept) {
+    accept.apply(new ItemSpecification<>(item, this));
   }
 
   @Override
-  public <S> void visit(S item, StructuralAccept<S> accept) {
-    accept.accept(new ItemSpecification<>(item, this));
+  protected <S> void visitItemIterator(Iterator<S> it, HashingAccept<S> hashingAccept) {
+    while (it.hasNext()) {
+      hashingAccept.acceptHashing(it.next(), this);
+    }
   }
 
   @Override
@@ -100,6 +119,27 @@
     }
 
     @Override
+    public ItemSpecification<T> withLong(ToLongFunction<T> getter) {
+      parent.visitLong(getter.applyAsLong(item));
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withDouble(ToDoubleFunction<T> getter) {
+      parent.visitDouble(getter.applyAsDouble(item));
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withIntArray(Function<T, int[]> getter) {
+      int[] ints = getter.apply(item);
+      for (int i = 0; i < ints.length; i++) {
+        parent.visitInt(ints[i]);
+      }
+      return this;
+    }
+
+    @Override
     protected <S> ItemSpecification<T> withConditionalCustomItem(
         Predicate<T> predicate,
         Function<T, S> getter,
@@ -109,9 +149,16 @@
       // Always hash the predicate result to distinguish, eg, {null, null} and {null}.
       parent.visitBool(test);
       if (test) {
-        hasher.accept(getter.apply(item), parent);
+        hasher.acceptHashing(getter.apply(item), parent);
       }
       return this;
     }
+
+    @Override
+    protected <S> ItemSpecification<T> withItemIterator(
+        Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+      parent.visitItemIterator(getter.apply(item), hasher);
+      return this;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java
index 1c6d25c..ede348f 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+/** Mapping of a specification over an item. */
+// TODO(b/171867022): Rename this to StructuralMapping to avoid confusion with the Acceptor and
+//  accept classes.
 @FunctionalInterface
 public interface StructuralAccept<T> {
-  void accept(StructuralSpecification<T, ?> visitor);
+  void apply(StructuralSpecification<T, ?> spec);
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralAcceptor.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralAcceptor.java
new file mode 100644
index 0000000..4a06e40
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralAcceptor.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+
+public interface StructuralAcceptor<T> extends CompareToAccept<T>, HashingAccept<T> {}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java
index f077c25..92542f2 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java
@@ -17,7 +17,7 @@
 
   @FunctionalInterface
   interface CompareToAccept<T> {
-    void accept(T item1, T item2, CompareToVisitor visitor);
+    void acceptCompareTo(T item1, T item2, CompareToVisitor visitor);
   }
 
   /**
@@ -51,7 +51,7 @@
 
   @FunctionalInterface
   interface HashingAccept<T> {
-    void accept(T item, HashingVisitor visitor);
+    void acceptHashing(T item, HashingVisitor visitor);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
index bb8f7c7..ff74111 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -5,19 +5,24 @@
 
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
 import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
 
 public abstract class StructuralSpecification<T, V extends StructuralSpecification<T, V>> {
 
   /**
-   * Basic specification for visiting an item.
+   * Base for accessing and visiting a sub-part on an item.
    *
-   * <p>This specified the getter for the item as well as all of the methods that are required for
-   * visiting. Those coincide with the requirements of Specified.
+   * <p>This specifies the getter for the sub-part as well as all of the methods that are required
+   * for visiting. The required methods coincide with the requirements of StructuralItem.
    *
-   * <p>It is preferable to use withStructuralItem.
+   * <p>It is preferable to use withItem and make the item itself implement StructuralItem.
    */
   @Deprecated
   public final <S> V withCustomItem(
@@ -25,12 +30,26 @@
     return withConditionalCustomItem(t -> true, getter, compare, hasher);
   }
 
+  public final <S> V withCustomItem(Function<T, S> getter, StructuralAcceptor<S> acceptor) {
+    return withCustomItem(getter, acceptor, acceptor);
+  }
+
+  /** Base implementation for visiting an item. */
   protected abstract <S> V withConditionalCustomItem(
       Predicate<T> predicate,
       Function<T, S> getter,
       CompareToAccept<S> compare,
       HashingAccept<S> hasher);
 
+  /** Base implementation for visiting an enumeration of items. */
+  protected abstract <S> V withItemIterator(
+      Function<T, Iterator<S>> getter, CompareToAccept<S> compare, HashingAccept<S> hasher);
+
+  public final <S> V withCustomItemCollection(
+      Function<T, Collection<S>> getter, StructuralAcceptor<S> acceptor) {
+    return withItemIterator(getter.andThen(Collection::iterator), acceptor, acceptor);
+  }
+
   /**
    * Specification for a "specified" item.
    *
@@ -41,7 +60,7 @@
     return withConditionalItem(t -> true, getter);
   }
 
-  final <S extends StructuralItem<S>> V withNullableItem(Function<T, S> getter) {
+  public final <S extends StructuralItem<S>> V withNullableItem(Function<T, S> getter) {
     return withConditionalItem(s -> getter.apply(s) != null, getter);
   }
 
@@ -50,6 +69,17 @@
     return withConditionalCustomItem(predicate, getter, S::acceptCompareTo, S::acceptHashing);
   }
 
+  public final <S extends StructuralItem<S>> V withItemCollection(
+      Function<T, Collection<S>> getter) {
+    return withItemIterator(
+        getter.andThen(Collection::iterator), S::acceptCompareTo, S::acceptHashing);
+  }
+
+  public final <S extends StructuralItem<S>> V withItemArray(Function<T, S[]> getter) {
+    return withItemIterator(
+        getter.andThen(a -> Arrays.asList(a).iterator()), S::acceptCompareTo, S::acceptHashing);
+  }
+
   /**
    * Helper to declare an assert on the item.
    *
@@ -62,4 +92,10 @@
   public abstract V withBool(Predicate<T> getter);
 
   public abstract V withInt(ToIntFunction<T> getter);
+
+  public abstract V withLong(ToLongFunction<T> getter);
+
+  public abstract V withDouble(ToDoubleFunction<T> getter);
+
+  public abstract V withIntArray(Function<T, int[]> getter);
 }
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index 09e32da..bda7fe1 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -48,7 +48,7 @@
     void run(DiagnosticsHandler handler) throws CompilationFailedException;
   }
 
-  private static void checkContains(String snippet, List<Diagnostic> diagnostics) {
+  public static void checkContains(String snippet, List<Diagnostic> diagnostics) {
     List<String> messages = ListUtils.map(diagnostics, Diagnostic::getDiagnosticMessage);
     System.out.println("Expecting match for '" + snippet + "'");
     System.out.println("StdErr:\n" + messages);
@@ -60,6 +60,10 @@
         diagnostics.stream().anyMatch(d -> d.getDiagnosticMessage().contains(snippet)));
   }
 
+  public static void checkContains(Collection<String> snippets, List<Diagnostic> diagnostics) {
+    snippets.forEach(snippet -> checkContains(snippet, diagnostics));
+  }
+
   public void checkErrorsContains(String snippet) {
     checkContains(snippet, errors);
   }
@@ -84,7 +88,20 @@
       runner.run(handler);
       fail("Failure expected");
     } catch (CompilationFailedException e) {
-      snippets.forEach(snippet -> checkContains(snippet, handler.errors));
+      checkContains(snippets, handler.errors);
+      throw e;
+    }
+  }
+
+  public static void checkErrorDiagnostics(
+      Consumer<DiagnosticsChecker> checker, FailingRunner runner)
+      throws CompilationFailedException {
+    DiagnosticsChecker handler = new DiagnosticsChecker();
+    try {
+      runner.run(handler);
+      fail("Failure expected");
+    } catch (CompilationFailedException e) {
+      checker.accept(handler);
       throw e;
     }
   }
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index b6fe80d..10b8afe 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -53,6 +53,12 @@
   // External JDK to use to run R8
   private final TestRuntime runtime;
 
+  // Enable/disable compiling with assertions
+  private boolean enableAssertions = true;
+
+  // Allow test proguard options
+  private boolean allowTestProguardOptions = false;
+
   private boolean addR8ExternalDeps = false;
 
   private List<String> jvmFlags = new ArrayList<>();
@@ -79,6 +85,16 @@
     return self();
   }
 
+  public ExternalR8TestBuilder enableAssertions(boolean enable) {
+    enableAssertions = enable;
+    return self();
+  }
+
+  public ExternalR8TestBuilder allowTestProguardOptions(boolean allow) {
+    allowTestProguardOptions = allow;
+    return self();
+  }
+
   @Override
   ExternalR8TestCompileResult internalCompile(
       Builder builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
@@ -101,9 +117,16 @@
 
       command.addAll(jvmFlags);
 
+      if (enableAssertions) {
+        command.add("-ea");
+      }
+
+      if (allowTestProguardOptions) {
+        command.add("-Dcom.android.tools.r8.allowTestProguardOptions=true");
+      }
+
       Collections.addAll(
           command,
-          "-ea",
           "-cp",
           classPath,
           R8.class.getTypeName(),
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java b/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java
index ea6b2f7..a7c0baf 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
@@ -41,14 +40,6 @@
     return proguardMap;
   }
 
-  public String stdout() {
-    return processResult.stdout;
-  }
-
-  public String stderr() {
-    return processResult.stdout;
-  }
-
   @Override
   public ExternalR8TestCompileResult self() {
     return this;
@@ -61,12 +52,12 @@
 
   @Override
   public String getStdout() {
-    throw new Unimplemented("Unexpected attempt to access stdout from external R8");
+    return processResult.stdout;
   }
 
   @Override
   public String getStderr() {
-    throw new Unimplemented("Unexpected attempt to access stderr from external R8");
+    return processResult.stderr;
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 058c2e5..34d573a 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -484,6 +484,10 @@
     return self();
   }
 
+  public T addNoHorizontalClassMergingRule(String clazz) {
+    return addKeepRules("-nohorizontalclassmerging class " + clazz);
+  }
+
   public T enableNoHorizontalClassMergingAnnotations() {
     if (!enableNoHorizontalClassMergingAnnotations) {
       enableNoHorizontalClassMergingAnnotations = true;
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index 7b630c3..f93d6d6 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -165,9 +165,13 @@
     return withApiFilter(api -> true);
   }
 
-  public TestParametersBuilder withAllApiLevelsAlsoForCf() {
+  public TestParametersBuilder enableApiLevelsForCf() {
     enableApiLevelsForCf = true;
-    return withAllApiLevels();
+    return this;
+  }
+
+  public TestParametersBuilder withAllApiLevelsAlsoForCf() {
+    return withAllApiLevels().enableApiLevelsForCf();
   }
 
   public TestParametersBuilder withApiLevel(AndroidApiLevel api) {
@@ -204,13 +208,8 @@
     if (!enableApiLevels) {
       return Stream.of(new TestParameters(runtime));
     }
-    if (!runtime.isDex()) {
-      if (!enableApiLevelsForCf) {
-        return Stream.of(new TestParameters(runtime));
-      }
-      return Stream.of(
-          new TestParameters(runtime, AndroidApiLevel.B),
-          new TestParameters(runtime, AndroidApiLevel.LATEST));
+    if (runtime.isCf() && !enableApiLevelsForCf) {
+      return Stream.of(new TestParameters(runtime));
     }
     List<AndroidApiLevel> sortedApiLevels =
         AndroidApiLevel.getAndroidApiLevelsSorted().stream()
@@ -219,7 +218,7 @@
     if (sortedApiLevels.isEmpty()) {
       return Stream.of();
     }
-    AndroidApiLevel vmLevel = runtime.asDex().getMinApiLevel();
+    AndroidApiLevel vmLevel = runtime.maxSupportedApiLevel();
     AndroidApiLevel lowestApplicable = sortedApiLevels.get(0);
     if (vmLevel.getLevel() < lowestApplicable.getLevel()) {
       return Stream.of();
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 99f9a71..db1ca7f 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -175,6 +175,12 @@
     }
 
     @Override
+    public AndroidApiLevel maxSupportedApiLevel() {
+      // The "none" runtime trivally supports all api levels as nothing is run.
+      return AndroidApiLevel.LATEST;
+    }
+
+    @Override
     public String toString() {
       return NAME;
     }
@@ -224,6 +230,11 @@
     }
 
     @Override
+    public AndroidApiLevel maxSupportedApiLevel() {
+      return getMinApiLevel();
+    }
+
+    @Override
     public boolean equals(Object other) {
       if (!(other instanceof DexRuntime)) {
         return false;
@@ -286,6 +297,12 @@
     }
 
     @Override
+    public AndroidApiLevel maxSupportedApiLevel() {
+      // TODO: define the mapping from android API levels back to JDKs.
+      return AndroidApiLevel.LATEST;
+    }
+
+    @Override
     public String toString() {
       return vm.toString();
     }
@@ -339,6 +356,8 @@
     throw new Unreachable("Unexpected runtime without backend: " + this);
   }
 
+  public abstract AndroidApiLevel maxSupportedApiLevel();
+
   @Override
   public abstract boolean equals(Object other);
 
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
index 8624277..8cd7860 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -165,6 +165,7 @@
             .enableNeverClassInliningAnnotations()
             .enableInliningAnnotations()
             .enableMemberValuePropagationAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .noMinification()
             .addKeepRules(
                 "-checkdiscard class " + Base.class.getCanonicalName() + "{",
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
index deb2559..c832fe7 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub1.java
@@ -5,8 +5,10 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
 @NeverClassInline
+@NoHorizontalClassMerging
 public class Sub1 extends Base implements Itf1 {
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
index 15cc549..ffdeec6 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privateinstance/Sub2.java
@@ -5,8 +5,10 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
 @NeverClassInline
+@NoHorizontalClassMerging
 public class Sub2 extends Base implements Itf2 {
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
index 070d6c8..f2660ef 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
@@ -257,8 +257,8 @@
             .setMode(mode)
             .compile();
     // Check that the process outputs (exit code, stdout, stderr) are the same.
-    assertEquals(result.stdout(), runR8R8.stdout());
-    assertEquals(result.stderr(), runR8R8.stderr());
+    assertEquals(result.getStdout(), runR8R8.getStdout());
+    assertEquals(result.getStderr(), runR8R8.getStderr());
     // Check that the output jars are the same.
     assertProgramsEqual(result.outputJar(), runR8R8.outputJar());
   }
diff --git a/src/test/java/com/android/tools/r8/cfmethodgeneration/MethodGenerationBase.java b/src/test/java/com/android/tools/r8/cfmethodgeneration/MethodGenerationBase.java
index 164259f..3ae4175 100644
--- a/src/test/java/com/android/tools/r8/cfmethodgeneration/MethodGenerationBase.java
+++ b/src/test/java/com/android/tools/r8/cfmethodgeneration/MethodGenerationBase.java
@@ -102,7 +102,8 @@
                 if (method.isInitializer()) {
                   continue;
                 }
-                String methodName = method.holder().getName() + "_" + method.method.name.toString();
+                String methodName =
+                    method.getHolderType().getName() + "_" + method.method.name.toString();
                 codePrinter.visitMethod(methodName, method.getCode().asCfCode());
               }
             });
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
index d1333a3..3c9d066 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
@@ -5,13 +5,13 @@
 package com.android.tools.r8.classmerging.horizontal;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class ClassesWithDifferentInterfacesTest extends HorizontalClassMergingTestBase {
@@ -31,8 +31,8 @@
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
-        .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(Z.class, Y.class))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("bar", "foo y", "bar")
         .inspect(
@@ -40,7 +40,8 @@
               assertThat(codeInspector.clazz(I.class), isPresent());
               assertThat(codeInspector.clazz(X.class), isPresent());
               assertThat(codeInspector.clazz(Y.class), isPresent());
-              assertThat(codeInspector.clazz(Z.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(Z.class), notIf(isPresent(), enableHorizontalClassMerging));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
index 5c03d3b..2bab975 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
@@ -30,14 +30,17 @@
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspectorIf(
-            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(Z.class, Y.class))
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector.assertMergedInto(Y.class, X.class).assertMergedInto(Z.class, X.class))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("bar", "foo y", "foo z")
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
               assertThat(codeInspector.clazz(X.class), isPresent());
-              assertThat(codeInspector.clazz(Y.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(Y.class), notIf(isPresent(), enableHorizontalClassMerging));
               assertThat(
                   codeInspector.clazz(Z.class), notIf(isPresent(), enableHorizontalClassMerging));
             });
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java
new file mode 100644
index 0000000..32f7454
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class ConstructorMergingAfterUnusedArgumentRemovalTest
+    extends HorizontalClassMergingTestBase {
+
+  public ConstructorMergingAfterUnusedArgumentRemovalTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector
+                    .assertMergedInto(B.class, A.class)
+                    .assertMergedInto(C.class, A.class)
+                    .assertMergedInto(D.class, A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "A.<init>(?, 43)", "B.<init>(44)", "C.<init>()", "D.<init>()");
+  }
+
+  @NeverClassInline
+  public static class A {
+
+    public A(int unused, int x) {
+      System.out.println("A.<init>(?, " + x + ")");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+
+    public B(int x) {
+      System.out.println("B.<init>(" + x + ")");
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+    public C() {
+      System.out.println("C.<init>()");
+    }
+  }
+
+  @NeverClassInline
+  public static class D {
+    public D() {
+      System.out.println("D.<init>()");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A(42, 43);
+      new B(44);
+      new C();
+      new D();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java
new file mode 100644
index 0000000..8b24e99
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ConflictWasDetectedTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.KeepUnusedArguments;
+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.codeinspector.VerticallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConflictWasDetectedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ConflictWasDetectedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        // .enableNoHorizontalClassMergingAnnotations()
+        .enableUnusedArgumentAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class);
+  }
+
+  static class Main {
+
+    public static void main(String... args) {
+      ConflictingInterfaceImpl impl = new ConflictingInterfaceImpl();
+      callMethodOnIface(impl);
+
+      // Ensure that the instantiations are not dead code eliminated.
+      escape(impl);
+    }
+
+    private static void callMethodOnIface(ConflictingInterface iface) {
+      System.out.println(iface.method());
+      System.out.println(ClassWithConflictingMethod.conflict(null));
+      System.out.println(OtherClassWithConflictingMethod.conflict(null));
+    }
+
+    @NeverInline
+    private static void escape(Object o) {
+      if (System.currentTimeMillis() < 0) {
+        System.out.println(o);
+      }
+    }
+  }
+
+  public interface ConflictingInterface {
+
+    String method();
+  }
+
+  public static class ConflictingInterfaceImpl implements ConflictingInterface {
+
+    @Override
+    public String method() {
+      return "ConflictingInterfaceImpl::method";
+    }
+  }
+
+  public static class ClassWithConflictingMethod {
+
+    @KeepUnusedArguments
+    public static int conflict(ConflictingInterface item) {
+      return 123;
+    }
+  }
+
+  public static class OtherClassWithConflictingMethod {
+
+    @KeepUnusedArguments
+    public static int conflict(ConflictingInterfaceImpl item) {
+      return 321;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/LambdaRewritingTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/LambdaRewritingTest.java
new file mode 100644
index 0000000..4cb9707
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/LambdaRewritingTest.java
@@ -0,0 +1,89 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.NeverInline;
+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;
+
+@RunWith(Parameterized.class)
+public class LambdaRewritingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public LambdaRewritingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addVerticallyMergedClassesInspector(
+            inspector -> inspector.assertMergedIntoSubtype(Interface.class))
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Interface obj = new InterfaceImpl();
+
+      // Leads to an invoke-custom instruction that mentions the type of `obj` since it is captured.
+      invoke(() -> obj.foo());
+
+      FunctionImpl functionImpl = new FunctionImpl();
+      if (System.currentTimeMillis() < 0) {
+        System.out.println(functionImpl);
+      }
+    }
+
+    @NeverInline
+    private static void invoke(Function f) {
+      f.accept();
+    }
+  }
+
+  // Cannot be merged as it has two subtypes: FunctionImpl and a lambda.
+  public interface Function {
+
+    void accept();
+  }
+
+  public static class FunctionImpl implements Function {
+
+    @Override
+    public void accept() {
+      System.out.println("In FunctionImpl.accept()");
+    }
+  }
+
+  // Will be merged into InterfaceImpl.
+  public interface Interface {
+
+    void foo();
+  }
+
+  public static class InterfaceImpl implements Interface {
+
+    @Override
+    public void foo() {
+      System.out.println("In InterfaceImpl.foo()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index d7d31c9..2fb36cd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -305,13 +305,6 @@
   }
 
   @Test
-  public void testConflictWasDetected() throws Throwable {
-    runR8(EXAMPLE_KEEP, this::configure);
-    assertThat(inspector.clazz("classmerging.ConflictingInterface"), isPresent());
-    assertThat(inspector.clazz("classmerging.ConflictingInterfaceImpl"), isPresent());
-  }
-
-  @Test
   public void testFieldCollision() throws Throwable {
     String main = "classmerging.FieldCollisionTest";
     Path[] programFiles =
@@ -334,34 +327,6 @@
   }
 
   @Test
-  public void testLambdaRewriting() throws Throwable {
-    String main = "classmerging.LambdaRewritingTest";
-    Path[] programFiles =
-        new Path[] {
-          JAVA8_CF_DIR.resolve("LambdaRewritingTest.class"),
-          JAVA8_CF_DIR.resolve("LambdaRewritingTest$Function.class"),
-          JAVA8_CF_DIR.resolve("LambdaRewritingTest$FunctionImpl.class"),
-          JAVA8_CF_DIR.resolve("LambdaRewritingTest$Interface.class"),
-          JAVA8_CF_DIR.resolve("LambdaRewritingTest$InterfaceImpl.class")
-        };
-    Set<String> preservedClassNames =
-        ImmutableSet.of(
-            "classmerging.LambdaRewritingTest",
-            "classmerging.LambdaRewritingTest$Function",
-            "classmerging.LambdaRewritingTest$FunctionImpl",
-            "classmerging.LambdaRewritingTest$InterfaceImpl");
-    runTestOnInput(
-        testForR8(parameters.getBackend())
-            .addKeepRules(getProguardConfig(JAVA8_EXAMPLE_KEEP))
-            .addOptionsModification(this::configure)
-            .addOptionsModification(options -> options.enableClassInlining = false)
-            .allowUnusedProguardConfigurationRules(),
-        main,
-        readProgramFiles(programFiles),
-        name -> preservedClassNames.contains(name) || name.contains("$Lambda$"));
-  }
-
-  @Test
   public void testMergeInterfaceWithoutInlining() throws Throwable {
     String main = "classmerging.ConflictingInterfaceSignaturesTest";
     Path[] programFiles =
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index b4a4c48..d094c78 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.TestBase;
@@ -49,7 +50,11 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withApiLevel(AndroidApiLevel.J).build();
+    return getTestParameters()
+        .withAllRuntimes()
+        .withApiLevel(AndroidApiLevel.J)
+        .enableApiLevelsForCf()
+        .build();
   }
 
   public BackportDuplicationTest(TestParameters parameters) {
@@ -58,6 +63,8 @@
 
   @Test
   public void testR8() throws Exception {
+    // R8 does not support desugaring with class file output so this test is only valid for DEX.
+    assumeTrue(parameters.isDexRuntime());
     runR8(false);
     runR8(true);
   }
@@ -87,6 +94,9 @@
 
   @Test
   public void testD8Merging() throws Exception {
+    assumeTrue(
+        "b/147485959: Merging does not happen for CF due to lack of synthetic annotations",
+        parameters.isDexRuntime());
     boolean intermediate = true;
     runD8Merging(intermediate);
   }
@@ -100,7 +110,7 @@
   private void runD8Merging(boolean intermediate) throws Exception {
     // Compile part 1 of the input (maybe intermediate)
     Path out1 =
-        testForD8()
+        testForD8(parameters.getBackend())
             .addProgramClasses(User1.class)
             .addClasspathClasses(CLASSES)
             .setMinApi(parameters.getApiLevel())
@@ -110,7 +120,7 @@
 
     // Compile part 2 of the input (maybe intermediate)
     Path out2 =
-        testForD8()
+        testForD8(parameters.getBackend())
             .addProgramClasses(User2.class)
             .addClasspathClasses(CLASSES)
             .setMinApi(parameters.getApiLevel())
@@ -126,7 +136,7 @@
     // Merge parts as an intermediate artifact.
     // This will not merge synthetics regardless of the setting of intermediate.
     Path out3 = temp.newFolder().toPath().resolve("out3.zip");
-    testForD8()
+    testForD8(parameters.getBackend())
         .addProgramClasses(MiniAssert.class, TestClass.class)
         .addProgramFiles(out1, out2)
         .setMinApi(parameters.getApiLevel())
@@ -139,7 +149,7 @@
         .inspect(inspector -> assertEquals(syntheticsInParts, getSyntheticMethods(inspector)));
 
     // Finally do a non-intermediate merge.
-    testForD8()
+    testForD8(parameters.getBackend())
         .addProgramFiles(out3)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
@@ -160,11 +170,13 @@
 
   @Test
   public void testD8FilePerClassFile() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
     runD8FilePerMode(OutputMode.DexFilePerClassFile);
   }
 
   @Test
   public void testD8FilePerClass() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
     runD8FilePerMode(OutputMode.DexFilePerClass);
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/EmptyDesugaredLibrary.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/EmptyDesugaredLibrary.java
index 3abe853..5b706e8 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/EmptyDesugaredLibrary.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/EmptyDesugaredLibrary.java
@@ -13,6 +13,8 @@
 import com.android.tools.r8.L8Command;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.StringResource;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.nio.charset.StandardCharsets;
@@ -21,9 +23,22 @@
 import java.util.Set;
 import java.util.zip.ZipFile;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class EmptyDesugaredLibrary extends DesugaredLibraryTestBase {
 
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public EmptyDesugaredLibrary(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
   private L8Command.Builder prepareL8Builder(AndroidApiLevel minApiLevel) {
     return L8Command.builder()
         .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java
index 2a968a0..46cb2bd 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java
@@ -112,7 +112,9 @@
                         diagnosticMessage(
                             containsString(
                                 "Code has already been library desugared. "
-                                    + "Interface Lj$/util/Iterator; is already implemented"))));
+                                    + "Interface Lj$/util/Iterator; is already implemented by "
+                                    + "Lcom/android/tools/r8/desugar/desugaredlibrary/"
+                                    + "IteratorTest$MyIterator;"))));
         fail("Expected failure");
       } catch (CompilationFailedException e) {
         // Expected.
@@ -124,7 +126,7 @@
         testForD8(Backend.CF)
             .addOptionsModification(
                 options ->
-                    options.desugarSpecificOptions().allowDesugaredInput =
+                    options.desugarSpecificOptions().allowAllDesugaredInput =
                         parameters.getApiLevel().isLessThan(apiLevelNotRequiringDesugaring))
             .setMinApi(parameters.getApiLevel())
             .addProgramFiles(firstJar)
@@ -141,9 +143,8 @@
     assertEquals(
         canUseDefaultAndStaticInterfaceMethods ? 0 : 1,
         info.getInterfaces().stream().filter(name -> name.equals("j$/util/Iterator")).count());
-    // TODO(b/171867367): This should only be 2.
     assertEquals(
-        canUseDefaultAndStaticInterfaceMethods ? 1 : 3,
+        canUseDefaultAndStaticInterfaceMethods ? 1 : 2,
         info.getMethodNames().stream().filter(name -> name.equals("forEachRemaining")).count());
 
     if (parameters.getRuntime().isDex()) {
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
index 210216f..cb8c24f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
@@ -127,6 +127,8 @@
     // Use D8 to desugar with Java classfile output.
     Path secondJar =
         testForD8(Backend.CF)
+            .addOptionsModification(
+                options -> options.desugarSpecificOptions().allowAllDesugaredInput = true)
             .setMinApi(parameters.getApiLevel())
             .addProgramFiles(firstJar)
             .addLibraryClasses(CustomLibClass.class)
@@ -141,8 +143,7 @@
     assertEquals(
         Impl.class.getTypeName(),
         DescriptorUtils.getJavaTypeFromBinaryName(info.getClassBinaryName()));
-    // TODO(b/171867367): This should only be 2.
-    assertEquals(3, info.getMethodNames().stream().filter(name -> name.equals("foo")).count());
+    assertEquals(2, info.getMethodNames().stream().filter(name -> name.equals("foo")).count());
 
     // Convert to DEX without desugaring and run.
     testForD8()
diff --git a/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java b/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
index 0fe19bc..7ea9110 100644
--- a/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
+++ b/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex;
 
+import static com.android.tools.r8.TestBase.getTestParameters;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
@@ -16,12 +20,26 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.Timing;
 import java.util.Collections;
 import org.junit.Assert;
 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 DebugByteCodeWriterTest {
 
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public DebugByteCodeWriterTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
   private ObjectToOffsetMapping emptyObjectTObjectMapping() {
     return new ObjectToOffsetMapping(
         AppView.createForD8(
@@ -39,7 +57,8 @@
         Collections.emptyList(),
         Collections.emptyList(),
         Collections.emptyList(),
-        Collections.emptyList());
+        Collections.emptyList(),
+        Timing.empty());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/DumpInputsTest.java b/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
similarity index 93%
rename from src/test/java/com/android/tools/r8/DumpInputsTest.java
rename to src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
index 84febed..008846d 100644
--- a/src/test/java/com/android/tools/r8/DumpInputsTest.java
+++ b/src/test/java/com/android/tools/r8/dump/DumpInputsTest.java
@@ -1,13 +1,17 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.dump;
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import java.io.IOException;
diff --git a/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java b/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java
new file mode 100644
index 0000000..ede7822
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dump/DumpMainDexInputsTest.java
@@ -0,0 +1,135 @@
+// 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.dump;
+
+import static java.util.stream.Collectors.toList;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+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.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DumpMainDexInputsTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public DumpMainDexInputsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private Path newMainDexListPath() throws IOException {
+    Path mainDexPath = temp.newFile("main-dex-list.txt").toPath();
+    String mainDexList =
+        StringUtils.lines(toMainDexFormat(MainDexFile1.class), toMainDexFormat(MainDexFile2.class));
+    Files.write(mainDexPath, mainDexList.getBytes());
+    return mainDexPath;
+  }
+
+  private static String toMainDexFormat(Class<?> clazz) {
+    return clazz.getName().replace(".", "/") + FileUtils.CLASS_EXTENSION;
+  }
+
+  @Test
+  public void testD8MainDexList() throws Exception {
+    Assume.assumeTrue(
+        "pre-native-multidex only",
+        parameters.getApiLevel().isLessThanOrEqualTo(AndroidApiLevel.K));
+    Path dumpDir = temp.newFolder().toPath();
+    testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addInnerClasses(DumpMainDexInputsTest.class)
+        .addMainDexListFiles(Collections.singleton(newMainDexListPath()))
+        .addMainDexListClasses(MainDexClass1.class, MainDexClass2.class, TestClass.class)
+        .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+        .addOptionsModification(options -> options.dumpInputToDirectory = dumpDir.toString())
+        .compile()
+        .assertAllInfoMessagesMatch(containsString("Dumped compilation inputs to:"))
+        .assertAllWarningMessagesMatch(
+            containsString(
+                "Dumping main dex list resources may have side effects due to I/O on Paths."))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
+    verifyDumpDir(dumpDir);
+  }
+
+  private void verifyDumpDir(Path dumpDir) throws IOException {
+    assertTrue(Files.isDirectory(dumpDir));
+    List<Path> paths = Files.walk(dumpDir, 1).collect(toList());
+    boolean hasVerified = false;
+    for (Path path : paths) {
+      if (!path.equals(dumpDir)) {
+        // The non-external run here results in assert code calling application read.
+        verifyDump(path);
+        hasVerified = true;
+      }
+    }
+    assertTrue(hasVerified);
+  }
+
+  private void verifyDump(Path dumpFile) throws IOException {
+    assertTrue(Files.exists(dumpFile));
+    Path unzipped = temp.newFolder().toPath();
+    ZipUtils.unzip(dumpFile.toString(), unzipped.toFile());
+    assertTrue(Files.exists(unzipped.resolve("r8-version")));
+    assertTrue(Files.exists(unzipped.resolve("program.jar")));
+    assertTrue(Files.exists(unzipped.resolve("library.jar")));
+    assertTrue(Files.exists(unzipped.resolve("main-dex-list.txt")));
+    String mainDex = new String(Files.readAllBytes(unzipped.resolve("main-dex-list.txt")));
+    List<String> mainDexLines =
+        Arrays.stream(mainDex.split("\n")).filter(s -> !s.isEmpty()).collect(toList());
+    assertEquals(5, mainDexLines.size());
+    List<String> expected =
+        Stream.of(
+                MainDexFile1.class,
+                MainDexFile2.class,
+                MainDexClass1.class,
+                MainDexClass2.class,
+                TestClass.class)
+            .map(DumpMainDexInputsTest::toMainDexFormat)
+            .collect(toList());
+    assertEquals(expected, mainDexLines);
+  }
+
+  static class TestClass {
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+
+  static class MainDexClass1 {}
+
+  static class MainDexClass2 {}
+
+  static class MainDexFile1 {}
+
+  static class MainDexFile2 {}
+
+  static class NonMainDex1 {}
+
+  static class NonMainDex2 {}
+}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
index 82f21d4..51b5323 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLookupTest.java
@@ -101,7 +101,7 @@
             .asLookupResultSuccess();
     assertNotNull(lookupResult);
     assertFalse(lookupResult.hasLambdaTargets());
-    if (subtypingInfo.subtypes(method.holder()).stream()
+    if (subtypingInfo.subtypes(method.getHolderType()).stream()
         .allMatch(t -> appInfo().definitionFor(t).isInterface())) {
       Counter counter = new Counter();
       lookupResult.forEach(
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
index cd743a9..d88e281 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -82,9 +82,13 @@
             .addKeepMainRule("proto2.TestClass")
             .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
             .addKeepRules(allGeneratedMessageLiteSubtypesAreInstantiatedRule())
+            // TODO(b/173340579): This rule should not be needed to allow shrinking of
+            //  PartiallyUsed$Enum.
+            .addNoHorizontalClassMergingRule(PARTIALLY_USED + "$Enum$1")
             .allowAccessModification(allowAccessModification)
             .allowDiagnosticMessages()
             .allowUnusedProguardConfigurationRules()
+            .enableProguardTestOptions()
             .enableProtoShrinking()
             .minification(enableMinification)
             .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index cf52085..e0266bd 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.classinliner;
 
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
@@ -88,9 +89,15 @@
             .enableSideEffectAnnotations()
             .addKeepMainRule(main)
             .addKeepAttributes("LineNumberTable")
+            .addHorizontallyMergedClassesInspector(
+                inspector ->
+                    inspector
+                        .assertMergedInto(Iface1Impl.class, CycleReferenceBA.class)
+                        .assertMergedInto(Iface2Impl.class, CycleReferenceBA.class))
             .allowAccessModification()
             .noMinification()
-            .run(main)
+            .setMinApi(parameters.getApiLevel())
+            .run(parameters.getRuntime(), main)
             .assertSuccessWithOutput(javaOutput);
 
     CodeInspector inspector = result.inspector();
@@ -143,7 +150,7 @@
             "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB"),
         collectTypes(inspector.clazz(CycleReferenceAB.class).uniqueMethodWithName("foo")));
 
-    assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
+    assertThat(inspector.clazz(CycleReferenceBA.class), isAbsent());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/ValuePropagationWithCatchHandlersTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/ValuePropagationWithCatchHandlersTest.java
new file mode 100644
index 0000000..32dbb5d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/ValuePropagationWithCatchHandlersTest.java
@@ -0,0 +1,69 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.membervaluepropagation;
+
+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 ValuePropagationWithCatchHandlersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ValuePropagationWithCatchHandlersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Woops!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      try {
+        test();
+      } catch (ExceptionInInitializerError e) {
+        System.out.println("Woops!");
+      }
+    }
+
+    static synchronized void test() {
+      System.out.println(Greeter.getInstance());
+    }
+  }
+
+  static class Greeter {
+
+    static final Greeter INSTANCE = new Greeter();
+
+    static {
+      if (System.currentTimeMillis() > 0) {
+        throw new RuntimeException();
+      }
+    }
+
+    public static Greeter getInstance() {
+      return INSTANCE;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
index 8dbe868..3e28157 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetClassTest.java
@@ -37,6 +37,7 @@
   @NoHorizontalClassMerging
   static class EffectivelyFinal {}
 
+  @NoHorizontalClassMerging
   static class Reflection implements Callable<Class<?>> {
 
     @ForceInline
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java
new file mode 100644
index 0000000..cbbd4f0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.switches;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SwitchMapInvalidOrdinalTest extends TestBase {
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public SwitchMapInvalidOrdinalTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addInnerClasses(SwitchMapInvalidOrdinalTest.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a", "b", "0", "a");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            "-keep class"
+                + "com.android.tools.r8.ir.optimize.switches.SwitchMapInvalidOrdinalTest$MyEnum {"
+                + " static <fields>; }")
+        .addInnerClasses(SwitchMapInvalidOrdinalTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        // When the code reaches the switch the first time, then the switch map int[] gets
+        // initialized based on the values in the enum at this point creating a mapping ordinal to
+        // switch map entry. Here D and X have 3 as ordinal.
+        .assertSuccessWithOutputLines("a", "b", "0", "a");
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C,
+    D;
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      try {
+        // Use reflection to instantiate a new enum instance, and set it to A, using getFields
+        // and not getField since A is minified.
+        Constructor<MyEnum> constructor =
+            (Constructor<MyEnum>) MyEnum.class.getDeclaredConstructors()[0];
+        constructor.setAccessible(true);
+        MyEnum x = constructor.newInstance("X", 3);
+        Field f = MyEnum.class.getFields()[0];
+
+        // On Android, setAccessible allows to set a final field.
+        f.setAccessible(true);
+
+        f.set(null, x);
+      } catch (Exception e) {
+        System.out.println("Unexpected: " + e);
+      }
+      print(MyEnum.A);
+      print(MyEnum.B);
+      print(MyEnum.C);
+      print(MyEnum.D);
+    }
+
+    private static void print(MyEnum e) {
+      switch (e) {
+        case A:
+          System.out.println("a");
+          break;
+        case B:
+          System.out.println("b");
+          break;
+        default:
+          System.out.println("0");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/b173184123/ClassExtendsInterfaceNamingTest.java b/src/test/java/com/android/tools/r8/naming/b173184123/ClassExtendsInterfaceNamingTest.java
new file mode 100644
index 0000000..fccc7b2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/b173184123/ClassExtendsInterfaceNamingTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming.b173184123;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+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.TestRuntime;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+// This is a reproduction of b/173184123.
+@RunWith(Parameterized.class)
+public class ClassExtendsInterfaceNamingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ClassExtendsInterfaceNamingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Path classFiles = temp.newFile("classes.jar").toPath();
+    ZipBuilder.builder(classFiles)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(),
+            ToolHelper.getClassFileForTestClass(Interface.class),
+            ToolHelper.getClassFileForTestClass(Main.class))
+        .addBytes(
+            DescriptorUtils.getPathFromJavaType(ConcreteClass.class),
+            transformer(ConcreteClass.class)
+                .setSuper(DescriptorUtils.javaTypeToDescriptor(Interface.class.getTypeName()))
+                .transform())
+        .build();
+    testForExternalR8(
+            parameters.getBackend(),
+            parameters.isCfRuntime() ? parameters.getRuntime() : TestRuntime.getCheckedInJdk11())
+        .addProgramFiles(classFiles)
+        .enableAssertions(false)
+        .useR8WithRelocatedDeps()
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndMembersRules(Interface.class)
+        .addKeepRules("-neverclassinline @com.android.tools.r8.NeverClassInline class *")
+        .addKeepRules("-neverinline class * { @**.NeverInline *; }")
+        .allowTestProguardOptions(true)
+        .compile()
+        .assertStderrThatMatches(
+            containsString(
+                "Class "
+                    + ConcreteClass.class.getTypeName()
+                    + " extends "
+                    + Interface.class.getTypeName()
+                    + " which is an interface"))
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+  }
+
+  public interface Interface {
+    void foo();
+  }
+
+  @NeverClassInline
+  public static class ConcreteClass /* extends InterfaceSub */ implements Interface {
+
+    @Override
+    @NeverInline
+    public void foo() {
+      System.out.println("foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new ConcreteClass().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java
new file mode 100644
index 0000000..a455d0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageProtectedInSeparatePackageTest extends RepackageTestBase {
+
+  private final String EXPECTED = "Base::foo";
+
+  public RepackageProtectedInSeparatePackageTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Base.class, Sub.class, Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8WithRepackage() throws Exception {
+    R8TestRunResult runResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(Base.class, Sub.class, Main.class)
+            .addKeepMainRule(Main.class)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepClassAndMembersRules(Base.class)
+            .apply(this::configureRepackaging)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .compile()
+            .inspect(
+                inspector -> {
+                  // TODO(b/173584786): We should not repackage Sub when generating CF.
+                  assertThat(Sub.class, isRepackaged(inspector));
+                })
+            .run(parameters.getRuntime(), Main.class);
+    if (parameters.isCfRuntime()) {
+      runResult.assertFailureWithErrorThatThrows(VerifyError.class);
+    } else {
+      runResult.assertSuccessWithOutputLines(EXPECTED);
+    }
+  }
+
+  public static class Base {
+    protected void foo() {
+      System.out.println("Base::foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class Sub extends Base {
+
+    @NeverInline
+    public void callFoo(Base base) {
+      base.foo();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new Sub().callFoo(new Sub());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryMethodTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryMethodTest.java
new file mode 100644
index 0000000..9eefd15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryMethodTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithPackagePrivateLibraryMethodTest extends RepackageTestBase {
+
+  private final String[] EXPECTED = new String[] {"Library::foo", "Program::bar"};
+
+  public RepackageWithPackagePrivateLibraryMethodTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addLibraryClasses(Library.class)
+        .addProgramClasses(Program.class, Main.class)
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestRunResult runResult =
+        testForR8(parameters.getBackend())
+            .addLibraryClasses(Library.class)
+            .addDefaultRuntimeLibrary(parameters)
+            .addProgramClasses(Program.class, Main.class)
+            .apply(this::configureRepackaging)
+            .addKeepMainRule(Main.class)
+            .enableInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+            .run(parameters.getRuntime(), Main.class);
+    if (parameters.isDexRuntime() && parameters.getDexRuntimeVersion() == Version.V8_1_0) {
+      runResult.assertSuccessWithOutputLines(EXPECTED);
+    } else {
+      runResult.assertFailureWithErrorThatThrows(IllegalAccessError.class);
+    }
+  }
+
+  public static class Library {
+
+    static void foo() {
+      System.out.println("Library::foo");
+    }
+  }
+
+  public static class Program {
+
+    @NeverInline
+    public static void bar() {
+      Library.foo();
+      System.out.println("Program::bar");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Program.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryOverrideTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryOverrideTest.java
new file mode 100644
index 0000000..7773fef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryOverrideTest.java
@@ -0,0 +1,81 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithPackagePrivateLibraryOverrideTest extends RepackageTestBase {
+
+  private final String[] EXPECTED = new String[] {"Program::foo"};
+
+  public RepackageWithPackagePrivateLibraryOverrideTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addLibraryClasses(Library.class)
+        .addProgramClasses(Program.class, Main.class)
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    R8TestRunResult runResult =
+        testForR8(parameters.getBackend())
+            .addLibraryClasses(Library.class)
+            .addDefaultRuntimeLibrary(parameters)
+            .addProgramClasses(Program.class, Main.class)
+            .apply(this::configureRepackaging)
+            .addKeepMainRule(Main.class)
+            .setMinApi(parameters.getApiLevel())
+            .enableNeverClassInliningAnnotations()
+            .compile()
+            .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+            .run(parameters.getRuntime(), Main.class);
+    if (parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik()) {
+      runResult.assertSuccessWithOutputLines(EXPECTED);
+    } else {
+      runResult.assertSuccessWithOutputLines("Library::foo");
+    }
+  }
+
+  public static class Library {
+
+    void foo() {
+      System.out.println("Library::foo");
+    }
+
+    public void callFoo() {
+      foo();
+    }
+  }
+
+  @NeverClassInline
+  public static class Program extends Library {
+
+    @Override
+    void foo() {
+      System.out.println("Program::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new Program().callFoo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibrarySuperTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibrarySuperTest.java
new file mode 100644
index 0000000..405c493
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibrarySuperTest.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithPackagePrivateLibrarySuperTest extends RepackageTestBase {
+
+  private final String[] EXPECTED = new String[] {"Library::foo"};
+
+  public RepackageWithPackagePrivateLibrarySuperTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addLibraryClasses(Library.class)
+        .addProgramClasses(Program.class, Main.class)
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryClasses(Library.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addProgramClasses(Program.class, Main.class)
+        .apply(this::configureRepackaging)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableNeverClassInliningAnnotations()
+        .compile()
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Library {
+
+    void foo() {
+      System.out.println("Library::foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class Program extends Library {
+
+    void callFoo() {
+      super.foo();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new Program().callFoo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryTypeTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryTypeTest.java
new file mode 100644
index 0000000..1a35ded
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageWithPackagePrivateLibraryTypeTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageWithPackagePrivateLibraryTypeTest extends RepackageTestBase {
+
+  private final String[] EXPECTED =
+      new String[] {
+        "class com.android.tools.r8.repackage.RepackageWithPackagePrivateLibraryTypeTest$Library",
+        "ProgramSub::foo",
+        "ProgramSub2::foo"
+      };
+
+  public RepackageWithPackagePrivateLibraryTypeTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addLibraryClasses(Library.class, LibraryI.class)
+        .addProgramClasses(Program.class, ProgramSub.class, ProgramSub2.class, Main.class)
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class, LibraryI.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addLibraryClasses(Library.class, LibraryI.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .addProgramClasses(Program.class, ProgramSub.class, ProgramSub2.class, Main.class)
+        .apply(this::configureRepackaging)
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, Library.class, LibraryI.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(IllegalAccessError.class);
+  }
+
+  static class Library {}
+
+  interface LibraryI {}
+
+  public static class Program {
+
+    @NeverInline
+    public static void bar() {
+      System.out.println(Library.class.toString());
+    }
+  }
+
+  @NeverClassInline
+  public static class ProgramSub extends Library {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("ProgramSub::foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class ProgramSub2 implements LibraryI {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("ProgramSub2::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Program.bar();
+      new ProgramSub().foo();
+      new ProgramSub2().foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
index bae2760..c6efef7 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetExecutionTest.java
@@ -99,6 +99,7 @@
         .addProgramClasses(CLASSES)
         .addProgramClassFileData(ASM_CLASSES)
         .addKeepMainRule(Main.class)
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
index c119db0..a702874 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
@@ -188,7 +188,8 @@
       Assert.assertNull(singleVirtualTarget);
     } else {
       Assert.assertNotNull(singleVirtualTarget);
-      Assert.assertEquals(toType(singleTargetHolderOrNull, appInfo), singleVirtualTarget.holder());
+      Assert.assertEquals(
+          toType(singleTargetHolderOrNull, appInfo), singleVirtualTarget.getHolderType());
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java
index 65e2ed5..6f14877 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialInterfaceMethodAccessTest.java
@@ -136,7 +136,7 @@
     DexEncodedMethod targetSuper =
         resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
     if (inSameNest) {
-      assertEquals(definingClassDefinition.type, targetSpecial.holder());
+      assertEquals(definingClassDefinition.type, targetSpecial.getHolderType());
       assertEquals(targetSpecial, targetSuper);
     } else {
       assertNull(targetSpecial);
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java
index 25a5d9e..6c7e5f2 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessTest.java
@@ -112,7 +112,7 @@
     DexEncodedMethod targetSuper =
         resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
     if (inSameNest) {
-      assertEquals(definingClassDefinition.type, targetSpecial.holder());
+      assertEquals(definingClassDefinition.type, targetSpecial.getHolderType());
       assertEquals(targetSpecial, targetSuper);
     } else {
       assertNull(targetSpecial);
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
index 4453f58..fe17f34 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodAccessWithIntermediateTest.java
@@ -145,7 +145,7 @@
     DexEncodedMethod targetSuper =
         resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
     if (inSameNest && symbolicReferenceIsDefiningType) {
-      assertEquals(definingClassDefinition.type, targetSpecial.holder());
+      assertEquals(definingClassDefinition.type, targetSpecial.getHolderType());
       assertEquals(targetSpecial, targetSuper);
     } else {
       assertNull(targetSpecial);
diff --git a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java
index aced457..9aa12b3 100644
--- a/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/access/NestInvokeSpecialMethodPublicAccessWithIntermediateTest.java
@@ -110,7 +110,7 @@
     // Verify that looking up the dispatch target returns the defining method.
     DexEncodedMethod targetSpecial =
         resolutionResult.lookupInvokeSpecialTarget(callerClassDefinition, appInfo);
-    assertEquals(definingClassDefinition.type, targetSpecial.holder());
+    assertEquals(definingClassDefinition.type, targetSpecial.getHolderType());
 
     DexEncodedMethod targetSuper =
         resolutionResult.lookupInvokeSuperTarget(callerClassDefinition, appInfo);
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/AbstractAllTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/AbstractAllTest.java
index b031eca..44f969f 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/AbstractAllTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/AbstractAllTest.java
@@ -48,7 +48,7 @@
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
     // Currently R8 will resolve to L::f as that is the first in the topological search.
     // Resolution may return any of the matches, so it is valid if this expectation changes.
-    assertEquals(L.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(L.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultLeftAbstractRightTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultLeftAbstractRightTest.java
index 429b8d9..990d146 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultLeftAbstractRightTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultLeftAbstractRightTest.java
@@ -51,7 +51,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(L.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(L.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultRightAbstractLeftTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultRightAbstractLeftTest.java
index a1e3185..32d88b3 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultRightAbstractLeftTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultRightAbstractLeftTest.java
@@ -51,7 +51,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(R.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(R.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
index 44c41eb..91b71ce 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractLeftTest.java
@@ -54,7 +54,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(L.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(L.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
index 7683037..61f9725 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAbstractRightTest.java
@@ -54,7 +54,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(R.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(R.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
index 6fa3216..5c8f813 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndBothTest.java
@@ -55,7 +55,7 @@
     Set<String> holders = new HashSet<>();
     resolutionResult
         .asFailedResolution()
-        .forEachFailureDependency(target -> holders.add(target.holder().toSourceString()));
+        .forEachFailureDependency(target -> holders.add(target.getHolderType().toSourceString()));
     assertEquals(ImmutableSet.of(L.class.getTypeName(), R.class.getTypeName()), holders);
   }
 
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndLeftTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndLeftTest.java
index ccac0ed..74fb903 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndLeftTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndLeftTest.java
@@ -45,7 +45,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(L.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(L.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndRightTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndRightTest.java
index 54a9b59..13f9340 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndRightTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/DefaultTopAndRightTest.java
@@ -45,7 +45,7 @@
     DexMethod method = buildNullaryVoidMethod(B.class, "f", appInfo.dexItemFactory());
     ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
     DexEncodedMethod resolutionTarget = resolutionResult.getSingleTarget();
-    assertEquals(R.class.getTypeName(), resolutionTarget.holder().toSourceString());
+    assertEquals(R.class.getTypeName(), resolutionTarget.getHolderType().toSourceString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
index a311b5a..8bc64cf 100644
--- a/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/interfacediamonds/TwoDefaultMethodsWithoutTopTest.java
@@ -56,7 +56,7 @@
     Set<String> holders = new HashSet<>();
     resolutionResult
         .asFailedResolution()
-        .forEachFailureDependency(m -> holders.add(m.holder().toSourceString()));
+        .forEachFailureDependency(m -> holders.add(m.getHolderType().toSourceString()));
     assertEquals(ImmutableSet.of(I.class.getTypeName(), J.class.getTypeName()), holders);
   }
 
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
index ff585af..c140fed 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassOne.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.resolution.singletarget.one;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+
+@NoHorizontalClassMerging
 public class SubSubClassOne extends AbstractSubClass implements IrrelevantInterfaceWithDefault {
 
   // Avoid SubSubClassOne.class.getCanonicalName() as it may change during shrinking.
diff --git a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassTwo.java b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassTwo.java
index bd4c45c..1534f36 100644
--- a/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassTwo.java
+++ b/src/test/java/com/android/tools/r8/resolution/singletarget/one/SubSubClassTwo.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.resolution.singletarget.one;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+
+@NoHorizontalClassMerging
 public class SubSubClassTwo extends AbstractSubClass {
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedDifferentPackageLookupTest.java b/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedDifferentPackageLookupTest.java
new file mode 100644
index 0000000..37b4dc8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedDifferentPackageLookupTest.java
@@ -0,0 +1,136 @@
+// 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.resolution.virtualtargets;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.LookupResult;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.google.common.collect.ImmutableList;
+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 ProtectedDifferentPackageLookupTest extends TestBase {
+
+  private static final String NEW_DESCRIPTOR_FOR_B = "Lanotherpackage/B;";
+  private static final String[] EXPECTED = new String[] {"A::foo", "A::foo"};
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ProtectedDifferentPackageLookupTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testResolution() throws Exception {
+    assumeTrue(parameters.useRuntimeAsNoneRuntime());
+    AndroidApp.Builder builder = AndroidApp.builder();
+    builder.addProgramFiles(ToolHelper.getClassFileForTestClass(A.class));
+    builder.addClassProgramData(
+        ImmutableList.of(getBInAnotherPackage(), getMainWithCallToRelocatedB()));
+    AppView<AppInfoWithLiveness> appView = computeAppViewWithLiveness(builder.build(), Main.class);
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(A.class, "foo", appInfo.dexItemFactory());
+    ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
+    DexProgramClass context =
+        appView.definitionForProgramType(buildType(Main.class, appInfo.dexItemFactory()));
+    LookupResult lookupResult = resolutionResult.lookupVirtualDispatchTargets(context, appInfo);
+    // TODO(b/173363527): Should be an error.
+    assertTrue(lookupResult.isLookupResultSuccess());
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    TestRunResult<?> runResult =
+        testForRuntime(parameters)
+            .addProgramClasses(A.class)
+            .addProgramClassFileData(getBInAnotherPackage(), getMainWithCallToRelocatedB())
+            .run(parameters.getRuntime(), Main.class);
+    if (parameters.isDexRuntime()) {
+      // TODO(b/173363527): Figure out if this should be an error on DEX
+      runResult.assertSuccessWithOutputLines(EXPECTED);
+    } else {
+      runResult.assertFailureWithErrorThatThrows(VerifyError.class);
+    }
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class)
+        .addProgramClassFileData(getBInAnotherPackage(), getMainWithCallToRelocatedB())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/173363527): Should be an error on CF.
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private byte[] getBInAnotherPackage() throws Exception {
+    return transformer(B.class).setClassDescriptor(NEW_DESCRIPTOR_FOR_B).transform();
+  }
+
+  private byte[] getMainWithCallToRelocatedB() throws Exception {
+    return transformer(Main.class)
+        .replaceClassDescriptorInMethodInstructions(
+            DescriptorUtils.javaTypeToDescriptor(B.class.getTypeName()), NEW_DESCRIPTOR_FOR_B)
+        .transform();
+  }
+
+  public static class A {
+
+    protected void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  // Will be moved to another package
+  public static class B extends A {
+
+    // Invoke-super is also legal but if we add a call to this we will not inline and we
+    // maintain the error.
+    // public void invokeSuper() {
+    // super.foo();
+    // }
+
+    public void invokeVirtual() {
+      foo(); // invoke-virtual
+    }
+
+    public void invokeOnMember(A a) {
+      // This one is illegal
+      a.foo();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B b = new B();
+      b.invokeVirtual();
+      b.invokeOnMember(new B());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedSamePackageLookupTest.java b/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedSamePackageLookupTest.java
new file mode 100644
index 0000000..ea86d8a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/virtualtargets/ProtectedSamePackageLookupTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.resolution.virtualtargets;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.LookupResult;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+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 ProtectedSamePackageLookupTest extends TestBase {
+
+  private static final String NEW_DESCRIPTOR_FOR_C = "Lanotherpackage/C;";
+  private static final String EXPECTED = "A::foo";
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ProtectedSamePackageLookupTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testResolution() throws Exception {
+    assumeTrue(parameters.useRuntimeAsNoneRuntime());
+    AppView<AppInfoWithLiveness> appView =
+        computeAppViewWithLiveness(
+            buildClasses(A.class, C.class, Main.class).build(), PackagePrivateChainTest.Main.class);
+    AppInfoWithLiveness appInfo = appView.appInfo();
+    DexMethod method = buildNullaryVoidMethod(A.class, "foo", appInfo.dexItemFactory());
+    ResolutionResult resolutionResult = appInfo.resolveMethodOnClass(method);
+    DexProgramClass context =
+        appView.definitionForProgramType(buildType(Main.class, appInfo.dexItemFactory()));
+    LookupResult lookupResult = resolutionResult.lookupVirtualDispatchTargets(context, appInfo);
+    assertTrue(lookupResult.isLookupResultSuccess());
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    TestRunResult<?> runResult =
+        testForRuntime(parameters)
+            .addProgramClasses(A.class, C.class, Main.class)
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(A.class, C.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class A {
+
+    protected void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  // Will be moved to another package
+  public static class C extends A {
+
+    public void runB(A b) {
+      b.foo();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new C().runB(new A());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumSideEffect.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumSideEffect.java
new file mode 100644
index 0000000..a0e59b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumSideEffect.java
@@ -0,0 +1,102 @@
+// 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.rewrite.enums;
+
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EnumSideEffect extends TestBase {
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EnumSideEffect(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8AndJava() throws Exception {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addInnerClasses(EnumSideEffect.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("clinit", "init", "init", "a", "b", "a", "b");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .addInnerClasses(EnumSideEffect.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("clinit", "a", "b", "init", "init", "a", "b");
+    // TODO(b/173580704): should be the following result as in D8:
+    // .assertSuccessWithOutputLines("clinit", "init", "init", "a", "b", "a", "b");
+  }
+
+  @NeverClassInline
+  enum MyEnum1 {
+    A,
+    B;
+
+    static {
+      System.out.println("clinit");
+    }
+  }
+
+  @NeverClassInline
+  enum MyEnum2 {
+    A,
+    B;
+
+    MyEnum2() {
+      System.out.println("init");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      switch1(MyEnum1.A);
+      switch1(MyEnum1.B);
+      switch2(MyEnum2.A);
+      switch2(MyEnum2.B);
+    }
+
+    @NeverInline
+    private static void switch1(MyEnum1 e) {
+      switch (e) {
+        case A:
+          System.out.println("a");
+          break;
+        case B:
+          System.out.println("b");
+          break;
+      }
+    }
+
+    @NeverInline
+    private static void switch2(MyEnum2 e) {
+      switch (e) {
+        case A:
+          System.out.println("a");
+          break;
+        case B:
+          System.out.println("b");
+          break;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
index d354df7..cbded10 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -75,7 +75,7 @@
   @Test(expected = CompilationFailedException.class)
   public void unsupportedCommandCommandLine() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "Missing command, specify one of 'check', '--print-usage' or '--keep-rules'",
+        "Missing command, specify one of 'check' or '--keep-rules'",
         handler -> {
           TraceReferences.run(
               TraceReferencesCommand.parse(new String[] {"--xxx"}, Origin.unknown(), handler)
@@ -152,7 +152,7 @@
   @Test(expected = CompilationFailedException.class)
   public void multipleFormatsCommandLine() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "Using '--output' requires command '--print-usage' or '--keep-rules'",
+        "Using '--output' requires command '--keep-rules'",
         handler -> {
           TraceReferences.run(
               TraceReferencesCommand.parse(
@@ -176,9 +176,6 @@
   }
 
   private String formatName(OutputFormat format) {
-    if (format == OutputFormat.PRINTUSAGE) {
-      return "--print-usage";
-    }
     if (format == OutputFormat.KEEP_RULES) {
       return "--keep-rules";
     }
@@ -187,13 +184,7 @@
   }
 
   enum OutputFormat {
-    /** Format used with the -printusage flag */
-    PRINTUSAGE,
-    /** Keep rules keeping each of the traced references */
     KEEP_RULES,
-    /**
-     * Keep rules with <code>allowobfuscation</code> modifier keeping each of the traced references
-     */
     KEEP_RULES_WITH_ALLOWOBFUSCATION
   }
 
@@ -229,12 +220,10 @@
     DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
     StringValueStringConsumer stringConsumer = new StringValueStringConsumer();
     TraceReferencesConsumer consumer =
-        format == OutputFormat.PRINTUSAGE
-            ? new TraceReferencesPrintUsage()
-            : TraceReferencesKeepRules.builder()
-                .setAllowObfuscation(format == OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION)
-                .setOutputConsumer(stringConsumer)
-                .build();
+        TraceReferencesKeepRules.builder()
+            .setAllowObfuscation(format == OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION)
+            .setOutputConsumer(stringConsumer)
+            .build();
     try {
       TraceReferences.run(
           TraceReferencesCommand.builder(diagnosticsChecker)
@@ -243,11 +232,7 @@
               .addSourceFiles(sourceJar)
               .setConsumer(consumer)
               .build());
-      if (consumer instanceof TraceReferencesPrintUsage) {
-        assertEquals(expected, ((TraceReferencesPrintUsage) consumer).get());
-      } else {
-        assertEquals(expected, stringConsumer.get());
-      }
+      assertEquals(expected, stringConsumer.get());
       if (diagnosticsCheckerConsumer != null) {
         diagnosticsCheckerConsumer.accept(diagnosticsChecker);
       } else {
@@ -315,21 +300,6 @@
   }
 
   @Test
-  public void test_printUses() throws Throwable {
-    runAndCheckOutput(
-        ImmutableList.of(Target.class),
-        ImmutableList.of(Source.class),
-        OutputFormat.PRINTUSAGE,
-        StringUtils.lines(
-            ImmutableList.of(
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
-                    + " method(int)",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
-                    + " field")));
-  }
-
-  @Test
   public void test_keepRules() throws Throwable {
     runAndCheckOutput(
         ImmutableList.of(Target.class),
@@ -409,11 +379,12 @@
     String expected =
         StringUtils.lines(
             ImmutableList.of(
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
-                    + " method(int)",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
-                    + " field"));
+                "-keep class"
+                    + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
+                "  public static void method(int);",
+                "  int field;",
+                "}",
+                "-keeppackagenames com.android.tools.r8.tracereferences"));
     Path dir = temp.newFolder().toPath();
     Path targetJar = zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(Target.class));
     Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
@@ -430,7 +401,7 @@
                     targetJar.toString(),
                     "--source",
                     sourceJar.toString(),
-                    "--print-usage",
+                    "--keep-rules",
                   },
                   Origin.unknown())
               .build());
@@ -440,16 +411,20 @@
     }
   }
 
+  @Test
   public void classFileInput() throws Throwable {
     String expected =
         StringUtils.lines(
             ImmutableList.of(
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
-                    + " method(int)",
-                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
-                    + " field"));
-    TraceReferencesPrintUsage consumer = new TraceReferencesPrintUsage();
+                "-keep class"
+                    + " com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target {",
+                "  public static void method(int);",
+                "  int field;",
+                "}",
+                "-keeppackagenames com.android.tools.r8.tracereferences"));
+    Path output = temp.newFile().toPath();
+    TraceReferencesKeepRules consumer =
+        TraceReferencesKeepRules.builder().setOutputPath(output).build();
     TraceReferences.run(
         TraceReferencesCommand.builder()
             .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
@@ -457,16 +432,21 @@
             .addSourceFiles(ToolHelper.getClassFileForTestClass(Source.class))
             .setConsumer(consumer)
             .build());
-    assertEquals(expected, consumer.get());
+    assertEquals(expected, FileUtils.readTextFile(output, Charsets.UTF_8));
 
-    Path output = temp.newFile().toPath();
+    output = temp.newFile().toPath();
     TraceReferences.run(
         TraceReferencesCommand.parse(
                 new String[] {
-                  "--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
-                  "--target", ToolHelper.getClassFileForTestClass(Target.class).toString(),
-                  "--source", ToolHelper.getClassFileForTestClass(Source.class).toString(),
-                  "--output", output.toString(),
+                  "--lib",
+                  ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                  "--target",
+                  ToolHelper.getClassFileForTestClass(Target.class).toString(),
+                  "--source",
+                  ToolHelper.getClassFileForTestClass(Source.class).toString(),
+                  "--output",
+                  output.toString(),
+                  "--keep-rules"
                 },
                 Origin.unknown())
             .build());
@@ -491,24 +471,7 @@
   }
 
   @Test
-  public void testNoReferences_printUses() throws Throwable {
-    try {
-      runAndCheckOutput(
-          ImmutableList.of(OtherTarget.class),
-          ImmutableList.of(Source.class),
-          OutputFormat.PRINTUSAGE,
-          StringUtils.lines(),
-          this::checkTargetMissing);
-      fail("Expected compilation to fail");
-    } catch (CompilationFailedException e) {
-      // Expected.
-    }
-  }
-
-  @Test
   public void testMissingReference_keepRules() throws Throwable {
-    Field field = Target.class.getField("field");
-    Method method = Target.class.getMethod("method", int.class);
     try {
       runAndCheckOutput(
           ImmutableList.of(OtherTarget.class),
@@ -584,30 +547,6 @@
   }
 
   @Test
-  public void testMissingDefinition_printUses() throws Throwable {
-    Path dir = temp.newFolder().toPath();
-    Path targetJar =
-        ZipBuilder.builder(dir.resolve("target.jar"))
-            .addBytes(
-                DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
-            .build();
-    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
-    try {
-      runAndCheckOutput(
-          targetJar,
-          sourceJar,
-          OutputFormat.PRINTUSAGE,
-          StringUtils.lines(
-              ImmutableList.of(
-                  "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target")),
-          this::checkTargetPartlyMissing);
-      fail("Expected compilation to fail");
-    } catch (CompilationFailedException e) {
-      // Expected.
-    }
-  }
-
-  @Test
   public void testMissingDefinition_keepRules() throws Throwable {
     Path dir = temp.newFolder().toPath();
     Path targetJar =
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
index 551c8a1..cad4538 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
@@ -12,13 +14,16 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -60,25 +65,55 @@
             .build();
 
     String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    List<String> snippets =
+        ImmutableList.of(
+            "Tracereferences found 3 classe(s), 2 field(s) and 4 method(s) without definition",
+            StringUtils.lines(
+                "Classe(s) without definition:",
+                prefix + "Target1;",
+                prefix + "Target2;",
+                prefix + "Target3;"),
+            StringUtils.lines(
+                "Field(s) without definition:",
+                prefix + "Target;missingField1:I",
+                prefix + "Target;missingField2:I"),
+            StringUtils.lines(
+                "Method(s) without definition:",
+                prefix + "Target1;<init>()V",
+                prefix + "Target2;<init>()V",
+                prefix + "Target3;<init>()V",
+                prefix + "Target;missingMethod()V"));
     try {
-      DiagnosticsChecker.checkErrorsContains(
-          ImmutableList.of(
-              "Tracereferences found 3 classe(s), 2 field(s) and 4 method(s) without definition",
-              StringUtils.lines(
-                  "Classe(s) without definition:",
-                  prefix + "Target1;",
-                  prefix + "Target2;",
-                  prefix + "Target3;"),
-              StringUtils.lines(
-                  "Field(s) without definition:",
-                  prefix + "Target;missingField1:I",
-                  prefix + "Target;missingField2:I"),
-              StringUtils.lines(
-                  "Method(s) without definition:",
-                  prefix + "Target1;<init>()V",
-                  prefix + "Target2;<init>()V",
-                  prefix + "Target3;<init>()V",
-                  prefix + "Target;missingMethod()V")),
+      DiagnosticsChecker.checkErrorDiagnostics(
+          checker -> {
+            DiagnosticsChecker.checkContains(snippets, checker.errors);
+            try {
+              assertEquals(1, checker.errors.size());
+              assertTrue(checker.errors.get(0) instanceof MissingDefinitionsDiagnostic);
+              MissingDefinitionsDiagnostic diagnostic =
+                  (MissingDefinitionsDiagnostic) checker.errors.get(0);
+              assertEquals(
+                  diagnostic.getMissingClasses(),
+                  ImmutableSet.of(
+                      Reference.classFromClass(Target1.class),
+                      Reference.classFromClass(Target2.class),
+                      Reference.classFromClass(Target3.class)));
+              assertEquals(
+                  diagnostic.getMissingFields(),
+                  ImmutableSet.of(
+                      Reference.fieldFromField(Target.class.getField("missingField1")),
+                      Reference.fieldFromField(Target.class.getField("missingField2"))));
+              assertEquals(
+                  diagnostic.getMissingMethods(),
+                  ImmutableSet.of(
+                      Reference.methodFromMethod(Target1.class.getDeclaredConstructor()),
+                      Reference.methodFromMethod(Target2.class.getDeclaredConstructor()),
+                      Reference.methodFromMethod(Target3.class.getDeclaredConstructor()),
+                      Reference.methodFromMethod(Target.class.getMethod("missingMethod"))));
+            } catch (ReflectiveOperationException e) {
+              fail("Unexpected exception");
+            }
+          },
           handler ->
               TraceReferences.run(
                   TraceReferencesCommand.builder(handler)
@@ -125,16 +160,37 @@
             .build();
 
     String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    List<String> snippets =
+        ImmutableList.of(
+            "Tracereferences found 2 field(s) and 1 method(s) without definition",
+            StringUtils.lines(
+                "Field(s) without definition:",
+                prefix + "Target;missingField1:I",
+                prefix + "Target;missingField2:I"),
+            StringUtils.lines("Method(s) without definition:", prefix + "Target;missingMethod()V"));
     try {
-      DiagnosticsChecker.checkErrorsContains(
-          ImmutableList.of(
-              "Tracereferences found 2 field(s) and 1 method(s) without definition",
-              StringUtils.lines(
-                  "Field(s) without definition:",
-                  prefix + "Target;missingField1:I",
-                  prefix + "Target;missingField2:I"),
-              StringUtils.lines(
-                  "Method(s) without definition:", prefix + "Target;missingMethod()V")),
+      DiagnosticsChecker.checkErrorDiagnostics(
+          checker -> {
+            DiagnosticsChecker.checkContains(snippets, checker.errors);
+            try {
+              assertEquals(1, checker.errors.size());
+              assertTrue(checker.errors.get(0) instanceof MissingDefinitionsDiagnostic);
+              MissingDefinitionsDiagnostic diagnostic =
+                  (MissingDefinitionsDiagnostic) checker.errors.get(0);
+              assertEquals(diagnostic.getMissingClasses(), ImmutableSet.of());
+              assertEquals(
+                  diagnostic.getMissingFields(),
+                  ImmutableSet.of(
+                      Reference.fieldFromField(Target.class.getField("missingField1")),
+                      Reference.fieldFromField(Target.class.getField("missingField2"))));
+              assertEquals(
+                  diagnostic.getMissingMethods(),
+                  ImmutableSet.of(
+                      Reference.methodFromMethod(Target.class.getMethod("missingMethod"))));
+            } catch (ReflectiveOperationException e) {
+              fail("Unexpected exception");
+            }
+          },
           handler ->
               TraceReferences.run(
                   TraceReferencesCommand.builder(handler)
@@ -175,12 +231,29 @@
             .build();
 
     String prefix = "  Lcom/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest$";
+    List<String> snippets =
+        ImmutableList.of(
+            "Tracereferences found 1 method(s) without definition",
+            StringUtils.lines("Method(s) without definition:", prefix + "Target;missingMethod()V"));
     try {
-      DiagnosticsChecker.checkErrorsContains(
-          ImmutableList.of(
-              "Tracereferences found 1 method(s) without definition",
-              StringUtils.lines(
-                  "Method(s) without definition:", prefix + "Target;missingMethod()V")),
+      DiagnosticsChecker.checkErrorDiagnostics(
+          checker -> {
+            DiagnosticsChecker.checkContains(snippets, checker.errors);
+            try {
+              assertEquals(1, checker.errors.size());
+              assertTrue(checker.errors.get(0) instanceof MissingDefinitionsDiagnostic);
+              MissingDefinitionsDiagnostic diagnostic =
+                  (MissingDefinitionsDiagnostic) checker.errors.get(0);
+              assertEquals(diagnostic.getMissingClasses(), ImmutableSet.of());
+              assertEquals(diagnostic.getMissingFields(), ImmutableSet.of());
+              assertEquals(
+                  diagnostic.getMissingMethods(),
+                  ImmutableSet.of(
+                      Reference.methodFromMethod(Target.class.getMethod("missingMethod"))));
+            } catch (ReflectiveOperationException e) {
+              fail("Unexpected exception");
+            }
+          },
           handler ->
               TraceReferences.run(
                   TraceReferencesCommand.builder(handler)
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index 764c1bb..7b61e06 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -292,6 +292,24 @@
         });
   }
 
+  public ClassFileTransformer setSuper(String superDescriptor) {
+    assert DescriptorUtils.isClassDescriptor(superDescriptor);
+    String newSuperName = getBinaryNameFromDescriptor(superDescriptor);
+    return addClassTransformer(
+        new ClassTransformer() {
+          @Override
+          public void visit(
+              int version,
+              int access,
+              String name,
+              String signature,
+              String superName,
+              String[] interfaces) {
+            super.visit(version, access, name, signature, newSuperName, interfaces);
+          }
+        });
+  }
+
   public ClassFileTransformer setAccessFlags(Consumer<ClassAccessFlags> fn) {
     return addClassTransformer(
         new ClassTransformer() {
diff --git a/tools/git_sync_cl_chain.py b/tools/git_sync_cl_chain.py
index 66713fa..f3a0728 100755
--- a/tools/git_sync_cl_chain.py
+++ b/tools/git_sync_cl_chain.py
@@ -199,15 +199,18 @@
   name = line[:name_end_index]
   line = line[name_end_index:].lstrip()
 
-  if ('[') not in line or ':' not in line:
-    return Repo(name, is_current, None)
+  if '[' in line:
+    line = line[line.index('[')+1:]
 
-  upstream_start_index = line.index('[')
-  line = line[upstream_start_index+1:]
-  upstream_end_index = line.index(':')
-  upstream = line[:upstream_end_index]
+    if ':' in line:
+      upstream = line[:line.index(':')]
+      return Repo(name, is_current, upstream)
 
-  return Repo(name, is_current, upstream)
+    if ']' in line:
+      upstream = line[:line.index(']')]
+      return Repo(name, is_current, upstream)
+
+  return Repo(name, is_current, None)
 
 if __name__ == '__main__':
   sys.exit(main(sys.argv[1:]))
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index 03687af..0ceb3b0b 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -319,6 +319,8 @@
     'url': 'https://github.com/chrisbanes/tivi',
     'revision': '8e2ddd8fe2d343264a66aa1ef8acbd4cc587e8ce',
     'folder': 'tivi',
+    # TODO(b/173974110): Enable recompilation.
+    'skip_recompilation': True,
   }),
   App({
     'id': 'com.keylesspalace.tusky',
@@ -348,6 +350,8 @@
     'url': 'https://github.com/android/compose-samples',
     'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
     'folder': 'android/compose-samples/crane',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
   }),
   # TODO(b/173167253): Check if monkey testing works.
   App({
@@ -398,6 +402,8 @@
     'url': 'https://github.com/android/compose-samples',
     'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
     'folder': 'android/compose-samples/jetsnack',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
   }),
   # TODO(b/173167253): Check if monkey testing works.
   App({
@@ -409,6 +415,8 @@
     'url': 'https://github.com/android/compose-samples',
     'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
     'folder': 'android/compose-samples/jetsurvey',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
   }),
   # TODO(b/173167253): Check if monkey testing works.
   App({
@@ -420,6 +428,8 @@
     'url': 'https://github.com/android/compose-samples',
     'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
     'folder': 'android/compose-samples/owl',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
   }),
   # TODO(b/173167253): Check if monkey testing works.
   App({
@@ -431,6 +441,8 @@
     'url': 'https://github.com/android/compose-samples',
     'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
     'folder': 'android/compose-samples/rally',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
   }),
 ]
 
@@ -790,6 +802,8 @@
       print('  {}-#{}:'.format(shrinker, compilation_index))
       dex_size = result.get('dex_size')
       msg = '    dex size: {}'.format(dex_size)
+      if options.print_runtimeraw:
+        print('    run time raw: {} ms'.format(result.get('duration')))
       if dex_size != proguard_dex_size and proguard_dex_size >= 0:
         msg = '{} ({}, {})'.format(
           msg, dex_size - proguard_dex_size,
@@ -887,6 +901,11 @@
                     help='Print the sizes of individual dex segments as ' +
                          '\'<BENCHMARKNAME>-<APP>-<segment>(CodeSize): '
                          '<bytes>\'')
+  result.add_option('--print-runtimeraw',
+                    metavar='BENCHMARKNAME',
+                    help='Print the line \'<BENCHMARKNAME>(RunTimeRaw):' +
+                        ' <elapsed> ms\' at the end where <elapsed> is' +
+                        ' the elapsed time in milliseconds.')
   result.add_option('--quiet',
                     help='Disable verbose logging',
                     default=False,