Merge commit 'e8fe8ec39fe72b9ba4c1b92abd920a828e09a2b0' into dev-release
diff --git a/build.gradle b/build.gradle
index 5153337..558919e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1938,11 +1938,6 @@
         systemProperty 'runtimes', project.property('runtimes')
     }
 
-    if (project.hasProperty('horizontalClassMerging')) {
-        println "NOTE: Running with horizontal class merging"
-        systemProperty 'com.android.tools.r8.horizontalClassMerging', 'true'
-    }
-
     if (project.hasProperty('slow_tests')) {
         systemProperty 'slow_tests', project.property('slow_tests')
     }
diff --git a/infra/config/global/cr-buildbucket.cfg b/infra/config/global/cr-buildbucket.cfg
index ee1883c..cd4e40c 100644
--- a/infra/config/global/cr-buildbucket.cfg
+++ b/infra/config/global/cr-buildbucket.cfg
@@ -176,15 +176,6 @@
       }
     }
     builders {
-      name: "linux_horizontal"
-      mixins: "linux"
-      mixins: "normal"
-      recipe {
-        properties: "tool:r8"
-        properties: "horizontal_class_merging:True"
-      }
-    }
-    builders {
       name: "linux_release"
       mixins: "normal"
       mixins: "linux"
diff --git a/infra/config/global/luci-milo.cfg b/infra/config/global/luci-milo.cfg
index bec8477..29b0731 100644
--- a/infra/config/global/luci-milo.cfg
+++ b/infra/config/global/luci-milo.cfg
@@ -31,11 +31,6 @@
     short_name: "jdk8_9"
   }
   builders {
-    name: "buildbucket/luci.r8.ci/linux_horizontal"
-    category: "R8"
-    short_name: "horizontal"
-  }
-  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-notify.cfg b/infra/config/global/luci-notify.cfg
index 4ed7592..040d32b 100644
--- a/infra/config/global/luci-notify.cfg
+++ b/infra/config/global/luci-notify.cfg
@@ -44,11 +44,6 @@
     repository: "https://r8.googlesource.com/r8"
   }
   builders {
-    name: "linux_horizontal"
-    bucket: "ci"
-    repository: "https://r8.googlesource.com/r8"
-  }
-  builders {
     name: "linux_release"
     bucket: "ci"
     repository: "https://r8.googlesource.com/r8"
diff --git a/infra/config/global/luci-scheduler.cfg b/infra/config/global/luci-scheduler.cfg
index 634885f..3c01353 100644
--- a/infra/config/global/luci-scheduler.cfg
+++ b/infra/config/global/luci-scheduler.cfg
@@ -30,7 +30,6 @@
   triggers: "linux-jdk8"
   triggers: "linux-jdk9"
   triggers: "linux-jdk8_9"
-  triggers: "linux_horizontal"
   triggers: "linux-android-4.0.4"
   triggers: "linux-android-4.4.4"
   triggers: "linux-android-5.1.1"
@@ -182,21 +181,6 @@
   }
 }
 
-
-job {
-  id: "linux_horizontal"
-  acl_sets: "default"
-  triggering_policy: {
-    kind: GREEDY_BATCHING
-    max_concurrent_invocations: 3
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.r8.ci"
-    builder: "linux_horizontal"
-  }
-}
-
 job {
   id: "linux-android-4.0.4"
   acl_sets: "default"
diff --git a/src/library_desugar/desugar_jdk_libs.json b/src/library_desugar/desugar_jdk_libs.json
index 945ad60..98ceb80 100644
--- a/src/library_desugar/desugar_jdk_libs.json
+++ b/src/library_desugar/desugar_jdk_libs.json
@@ -2,9 +2,10 @@
   "configuration_format_version": 3,
   "group_id" : "com.tools.android",
   "artifact_id" : "desugar_jdk_libs",
-  "version": "1.0.12",
+  "version": "1.1.0",
   "required_compilation_api_level": 26,
   "synthesized_library_classes_package_prefix": "j$.",
+  "support_all_callbacks_from_library": true,
   "common_flags": [
     {
       "api_level_below_or_equal": 25,
diff --git a/src/library_desugar/desugar_jdk_libs_alternative_3.json b/src/library_desugar/desugar_jdk_libs_alternative_3.json
new file mode 100644
index 0000000..628fc53
--- /dev/null
+++ b/src/library_desugar/desugar_jdk_libs_alternative_3.json
@@ -0,0 +1,258 @@
+{
+  "configuration_format_version": 3,
+  "group_id" : "com.tools.android",
+  "artifact_id" : "desugar_jdk_libs_alternative_3",
+  "version": "1.1.0",
+  "required_compilation_api_level": 26,
+  "synthesized_library_classes_package_prefix": "j$.",
+  "support_all_callbacks_from_library": false,
+  "common_flags": [
+    {
+      "api_level_below_or_equal": 25,
+      "wrapper_conversion": [
+        "java.time.Clock"
+      ]
+    },
+    {
+      "api_level_below_or_equal": 23,
+      "wrapper_conversion": [
+        "java.util.PrimitiveIterator$OfDouble",
+        "java.util.PrimitiveIterator$OfInt",
+        "java.util.PrimitiveIterator$OfLong",
+        "java.util.Spliterator",
+        "java.util.Spliterator$OfDouble",
+        "java.util.Spliterator$OfInt",
+        "java.util.Spliterator$OfLong",
+        "java.util.Spliterator$OfPrimitive",
+        "java.util.function.BiConsumer",
+        "java.util.function.BiFunction",
+        "java.util.function.BiPredicate",
+        "java.util.function.BinaryOperator",
+        "java.util.function.Consumer",
+        "java.util.function.DoubleBinaryOperator",
+        "java.util.function.DoubleConsumer",
+        "java.util.function.DoubleFunction",
+        "java.util.function.DoublePredicate",
+        "java.util.function.DoubleToIntFunction",
+        "java.util.function.DoubleToLongFunction",
+        "java.util.function.DoubleUnaryOperator",
+        "java.util.function.Function",
+        "java.util.function.IntBinaryOperator",
+        "java.util.function.IntConsumer",
+        "java.util.function.IntFunction",
+        "java.util.function.IntPredicate",
+        "java.util.function.IntToDoubleFunction",
+        "java.util.function.IntToLongFunction",
+        "java.util.function.IntUnaryOperator",
+        "java.util.function.LongBinaryOperator",
+        "java.util.function.LongConsumer",
+        "java.util.function.LongFunction",
+        "java.util.function.LongPredicate",
+        "java.util.function.LongToDoubleFunction",
+        "java.util.function.LongToIntFunction",
+        "java.util.function.LongUnaryOperator",
+        "java.util.function.ObjDoubleConsumer",
+        "java.util.function.ObjIntConsumer",
+        "java.util.function.ObjLongConsumer",
+        "java.util.function.Predicate",
+        "java.util.function.Supplier",
+        "java.util.function.ToDoubleFunction",
+        "java.util.function.ToIntFunction",
+        "java.util.function.ToLongFunction",
+        "java.util.function.UnaryOperator",
+        "java.util.stream.BaseStream",
+        "java.util.stream.Collector",
+        "java.util.stream.DoubleStream",
+        "java.util.stream.IntStream",
+        "java.util.stream.LongStream",
+        "java.util.stream.Stream"
+      ]
+    }
+  ],
+  "library_flags": [
+    {
+      "api_level_below_or_equal": 25,
+      "rewrite_prefix": {
+        "j$.time.": "java.time.",
+        "java.time.": "j$.time.",
+        "java.util.Desugar": "j$.util.Desugar"
+      },
+      "backport": {
+        "java.lang.Double8": "java.lang.Double",
+        "java.lang.Integer8": "java.lang.Integer",
+        "java.lang.Long8": "java.lang.Long",
+        "java.lang.Math8": "java.lang.Math"
+      },
+      "retarget_lib_member": {
+        "java.util.Date#toInstant": "java.util.DesugarDate",
+        "java.util.GregorianCalendar#toZonedDateTime": "java.util.DesugarGregorianCalendar",
+        "java.util.TimeZone#toZoneId": "java.util.DesugarTimeZone"
+      },
+      "custom_conversion": {
+        "java.time.ZonedDateTime": "java.time.TimeConversions",
+        "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.Duration": "java.time.TimeConversions",
+        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Instant": "java.time.TimeConversions"
+      }
+    },
+    {
+      "api_level_below_or_equal": 23,
+      "rewrite_prefix": {
+        "j$.util.Optional": "java.util.Optional",
+        "j$.util.LongSummaryStatistics": "java.util.LongSummaryStatistics",
+        "j$.util.IntSummaryStatistics": "java.util.IntSummaryStatistics",
+        "j$.util.DoubleSummaryStatistics": "java.util.DoubleSummaryStatistics",
+        "java.util.stream.": "j$.util.stream.",
+        "java.util.function.": "j$.util.function.",
+        "java.util.Comparators": "j$.util.Comparators",
+        "java.util.DoubleSummaryStatistics": "j$.util.DoubleSummaryStatistics",
+        "java.util.IntSummaryStatistics": "j$.util.IntSummaryStatistics",
+        "java.util.LongSummaryStatistics": "j$.util.LongSummaryStatistics",
+        "java.util.Objects": "j$.util.Objects",
+        "java.util.Optional": "j$.util.Optional",
+        "java.util.PrimitiveIterator": "j$.util.PrimitiveIterator",
+        "java.util.SortedSet$1": "j$.util.SortedSet$1",
+        "java.util.Spliterator": "j$.util.Spliterator",
+        "java.util.StringJoiner": "j$.util.StringJoiner",
+        "java.util.Tripwire": "j$.util.Tripwire",
+        "java.util.concurrent.DesugarUnsafe": "j$.util.concurrent.DesugarUnsafe",
+        "java.util.concurrent.ThreadLocalRandom": "j$.util.concurrent.ThreadLocalRandom",
+        "java.util.concurrent.atomic.DesugarAtomic": "j$.util.concurrent.atomic.DesugarAtomic",
+        "java.util.concurrent.ConcurrentHashMap": "j$.util.concurrent.ConcurrentHashMap",
+        "java.io.DesugarBufferedReader": "j$.io.DesugarBufferedReader",
+        "java.io.UncheckedIOException": "j$.io.UncheckedIOException"
+      },
+      "retarget_lib_member": {
+        "java.util.Arrays#stream": "java.util.DesugarArrays",
+        "java.util.Arrays#spliterator": "java.util.DesugarArrays",
+        "java.util.LinkedHashSet#spliterator": "java.util.DesugarLinkedHashSet",
+        "java.io.BufferedReader#lines": "java.io.DesugarBufferedReader"
+      },
+      "dont_rewrite": [
+        "java.util.Iterator#remove"
+      ],
+      "emulate_interface": {
+        "java.util.Map$Entry": "j$.util.Map$Entry",
+        "java.util.Collection": "j$.util.Collection",
+        "java.util.Map": "j$.util.Map",
+        "java.util.Iterator": "j$.util.Iterator",
+        "java.util.Comparator": "j$.util.Comparator",
+        "java.util.List": "j$.util.List",
+        "java.util.SortedSet": "j$.util.SortedSet",
+        "java.util.Set": "j$.util.Set",
+        "java.util.concurrent.ConcurrentMap": "j$.util.concurrent.ConcurrentMap"
+      },
+      "custom_conversion": {
+        "java.util.Optional": "java.util.OptionalConversions",
+        "java.util.OptionalDouble": "java.util.OptionalConversions",
+        "java.util.OptionalInt": "java.util.OptionalConversions",
+        "java.util.OptionalLong": "java.util.OptionalConversions",
+        "java.util.LongSummaryStatistics": "java.util.LongSummaryStatisticsConversions",
+        "java.util.IntSummaryStatistics": "java.util.IntSummaryStatisticsConversions",
+        "java.util.DoubleSummaryStatistics": "java.util.DoubleSummaryStatisticsConversions"
+      }
+    }
+  ],
+  "program_flags": [
+    {
+      "api_level_below_or_equal": 25,
+      "rewrite_prefix": {
+        "java.time.": "j$.time.",
+        "java.util.Desugar": "j$.util.Desugar"
+      },
+      "retarget_lib_member": {
+        "java.util.Calendar#toInstant": "java.util.DesugarCalendar",
+        "java.util.Date#from": "java.util.DesugarDate",
+        "java.util.Date#toInstant": "java.util.DesugarDate",
+        "java.util.GregorianCalendar#from": "java.util.DesugarGregorianCalendar",
+        "java.util.GregorianCalendar#toZonedDateTime": "java.util.DesugarGregorianCalendar",
+        "java.util.TimeZone#getTimeZone": "java.util.DesugarTimeZone",
+        "java.util.TimeZone#toZoneId": "java.util.DesugarTimeZone"
+      },
+      "custom_conversion": {
+        "java.time.ZonedDateTime": "java.time.TimeConversions",
+        "java.time.LocalDate": "java.time.TimeConversions",
+        "java.time.Duration": "java.time.TimeConversions",
+        "java.time.ZoneId": "java.time.TimeConversions",
+        "java.time.MonthDay": "java.time.TimeConversions",
+        "java.time.Instant": "java.time.TimeConversions"
+      }
+    },
+    {
+      "api_level_below_or_equal": 23,
+      "rewrite_prefix": {
+        "java.util.stream.": "j$.util.stream.",
+        "java.util.function.": "j$.util.function.",
+        "java.util.DoubleSummaryStatistics": "j$.util.DoubleSummaryStatistics",
+        "java.util.IntSummaryStatistics": "j$.util.IntSummaryStatistics",
+        "java.util.LongSummaryStatistics": "j$.util.LongSummaryStatistics",
+        "java.util.Optional": "j$.util.Optional",
+        "java.util.PrimitiveIterator": "j$.util.PrimitiveIterator",
+        "java.util.Spliterator": "j$.util.Spliterator",
+        "java.util.StringJoiner": "j$.util.StringJoiner",
+        "java.util.concurrent.ThreadLocalRandom": "j$.util.concurrent.ThreadLocalRandom",
+        "java.util.concurrent.atomic.DesugarAtomic": "j$.util.concurrent.atomic.DesugarAtomic",
+        "java.util.concurrent.ConcurrentHashMap": "j$.util.concurrent.ConcurrentHashMap",
+        "java.io.UncheckedIOException": "j$.io.UncheckedIOException"
+      },
+      "retarget_lib_member": {
+        "java.util.Arrays#stream": "java.util.DesugarArrays",
+        "java.util.Arrays#spliterator": "java.util.DesugarArrays",
+        "java.util.LinkedHashSet#spliterator": "java.util.DesugarLinkedHashSet",
+        "java.util.concurrent.atomic.AtomicInteger#getAndUpdate": "java.util.concurrent.atomic.DesugarAtomicInteger",
+        "java.util.concurrent.atomic.AtomicInteger#updateAndGet": "java.util.concurrent.atomic.DesugarAtomicInteger",
+        "java.util.concurrent.atomic.AtomicInteger#getAndAccumulate": "java.util.concurrent.atomic.DesugarAtomicInteger",
+        "java.util.concurrent.atomic.AtomicInteger#accumulateAndGet": "java.util.concurrent.atomic.DesugarAtomicInteger",
+        "java.util.concurrent.atomic.AtomicLong#getAndUpdate": "java.util.concurrent.atomic.DesugarAtomicLong",
+        "java.util.concurrent.atomic.AtomicLong#updateAndGet": "java.util.concurrent.atomic.DesugarAtomicLong",
+        "java.util.concurrent.atomic.AtomicLong#getAndAccumulate": "java.util.concurrent.atomic.DesugarAtomicLong",
+        "java.util.concurrent.atomic.AtomicLong#accumulateAndGet": "java.util.concurrent.atomic.DesugarAtomicLong",
+        "java.util.concurrent.atomic.AtomicReference#getAndUpdate": "java.util.concurrent.atomic.DesugarAtomicReference",
+        "java.util.concurrent.atomic.AtomicReference#updateAndGet": "java.util.concurrent.atomic.DesugarAtomicReference",
+        "java.util.concurrent.atomic.AtomicReference#getAndAccumulate": "java.util.concurrent.atomic.DesugarAtomicReference",
+        "java.util.concurrent.atomic.AtomicReference#accumulateAndGet": "java.util.concurrent.atomic.DesugarAtomicReference",
+        "java.util.Collections#synchronizedMap": "java.util.DesugarCollections",
+        "java.util.Collections#synchronizedSortedMap": "java.util.DesugarCollections",
+        "java.io.BufferedReader#lines": "java.io.DesugarBufferedReader"
+      },
+      "dont_rewrite": [
+        "java.util.Iterator#remove"
+      ],
+      "emulate_interface": {
+        "java.util.Map$Entry": "j$.util.Map$Entry",
+        "java.util.Collection": "j$.util.Collection",
+        "java.util.Map": "j$.util.Map",
+        "java.util.Iterator": "j$.util.Iterator",
+        "java.util.Comparator": "j$.util.Comparator",
+        "java.util.List": "j$.util.List",
+        "java.util.SortedSet": "j$.util.SortedSet",
+        "java.util.Set": "j$.util.Set",
+        "java.util.concurrent.ConcurrentMap": "j$.util.concurrent.ConcurrentMap"
+      },
+      "custom_conversion": {
+        "java.util.Optional": "java.util.OptionalConversions",
+        "java.util.OptionalDouble": "java.util.OptionalConversions",
+        "java.util.OptionalInt": "java.util.OptionalConversions",
+        "java.util.OptionalLong": "java.util.OptionalConversions",
+        "java.util.LongSummaryStatistics": "java.util.LongSummaryStatisticsConversions",
+        "java.util.IntSummaryStatistics": "java.util.IntSummaryStatisticsConversions",
+        "java.util.DoubleSummaryStatistics": "java.util.DoubleSummaryStatisticsConversions"
+      }
+    }
+  ],
+  "shrinker_config": [
+    "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$TreeBin { int lockState; }",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap { int sizeCtl; int transferIndex; long baseCount; int cellsBusy; }",
+    "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$CounterCell { long value; }",
+    "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }",
+    "-keeppackagenames j$",
+    "-keepclassmembers class j$.util.IntSummaryStatistics { long count; long sum; int min; int max; }",
+    "-keepclassmembers class j$.util.LongSummaryStatistics { long count; long sum; long min; long max; }",
+    "-keepclassmembers class j$.util.DoubleSummaryStatistics { long count; double sum; double min; double max; }",
+    "-keepattributes Signature",
+    "-keepattributes EnclosingMethod",
+    "-keepattributes InnerClasses"
+  ]
+}
diff --git a/src/main/java/com/android/tools/r8/DataEntryResource.java b/src/main/java/com/android/tools/r8/DataEntryResource.java
index 6b8f9c5..e20e5b2 100644
--- a/src/main/java/com/android/tools/r8/DataEntryResource.java
+++ b/src/main/java/com/android/tools/r8/DataEntryResource.java
@@ -26,6 +26,15 @@
     return new ByteDataEntryResource(bytes, name, origin);
   }
 
+  static DataEntryResource fromString(String name, Origin origin, String... lines) {
+    StringBuilder sb = new StringBuilder();
+    for (String line : lines) {
+      sb.append(line);
+      sb.append(System.lineSeparator());
+    }
+    return new ByteDataEntryResource(sb.toString().getBytes(), name, origin);
+  }
+
   static DataEntryResource fromFile(Path dir, Path file) {
     return new LocalDataEntryResource(dir.resolve(file).toFile(),
         file.toString().replace(File.separatorChar, SEPARATOR));
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index dad6c63..ebc0bc6 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -532,7 +532,7 @@
                   mainDexTracingResult);
           VerticalClassMergerGraphLens lens = verticalClassMerger.run();
           if (lens != null) {
-            appView.setVerticallyMergedClasses(verticalClassMerger.getMergedClasses());
+            appView.setVerticallyMergedClasses(lens.getMergedClasses());
             appView.rewriteWithLens(lens);
             runtimeTypeCheckInfo = runtimeTypeCheckInfo.rewriteWithLens(lens);
           }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f9c2050..bbe9387 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -861,11 +861,16 @@
       internal.enableClassStaticizer = false;
       internal.outline.enabled = false;
       internal.enableEnumUnboxing = false;
+      internal.enableLambdaMerging = false;
     }
     if (!internal.isShrinking()) {
-      // If R8 is not shrinking, there is no point in unboxing enums since the unboxed enums
-      // will still remain in the program (The application size would actually increase).
+      // If R8 is not shrinking, there is no point in running various optimizations since the
+      // optimized classes will still remain in the program (the application size could increase).
       internal.enableEnumUnboxing = false;
+      internal.enableHorizontalClassMerging = false;
+      internal.enableLambdaMerging = false;
+      internal.enableStaticClassMerging = false;
+      internal.enableVerticalClassMerging = false;
     }
 
     if (!internal.enableInlining) {
@@ -929,6 +934,7 @@
     if (internal.isGeneratingClassFiles()) {
       internal.outline.enabled = false;
       internal.enableEnumUnboxing = false;
+      internal.enableHorizontalClassMerging = false;
     }
 
     // EXPERIMENTAL flags.
diff --git a/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java b/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java
new file mode 100644
index 0000000..cb4fafb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java
@@ -0,0 +1,91 @@
+// 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.errors;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+@Keep
+public class AssumeNoSideEffectsRuleForObjectMembersDiagnostic implements Diagnostic {
+
+  private final List<MethodReference> methods;
+  private final Origin origin;
+  private final Position position;
+
+  private AssumeNoSideEffectsRuleForObjectMembersDiagnostic(
+      List<MethodReference> methods, Origin origin, Position position) {
+    this.methods = methods;
+    this.origin = origin;
+    this.position = position;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    Iterator<MethodReference> iterator = methods.iterator();
+    StringBuilder message =
+        new StringBuilder("The -assumenosideeffects rule matches the following method(s) ")
+            .append("on java.lang.Object: ")
+            .append(MethodReferenceUtils.toSourceStringWithoutHolderAndReturnType(iterator.next()));
+    while (iterator.hasNext()) {
+      MethodReference method = iterator.next();
+      message
+          .append(iterator.hasNext() ? ", " : " and ")
+          .append(MethodReferenceUtils.toSourceStringWithoutHolderAndReturnType(method));
+    }
+    return message
+        .append(". ")
+        .append("This is most likely not intended. ")
+        .append("Consider specifying the methods more precisely.")
+        .toString();
+  }
+
+  public static class Builder {
+
+    private final List<MethodReference> methods = new ArrayList<>();
+    private Origin origin;
+    private Position position;
+
+    public Builder() {}
+
+    public Builder addMatchedMethods(Set<DexMethod> methods) {
+      for (DexMethod method : methods) {
+        this.methods.add(method.asMethodReference());
+      }
+      return this;
+    }
+
+    public Builder setOrigin(Origin origin) {
+      this.origin = origin;
+      return this;
+    }
+
+    public Builder setPosition(Position position) {
+      this.position = position;
+      return this;
+    }
+
+    public AssumeNoSideEffectsRuleForObjectMembersDiagnostic build() {
+      return new AssumeNoSideEffectsRuleForObjectMembersDiagnostic(methods, origin, position);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 114bc8a..3c5f4b7 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
+import java.util.List;
 import java.util.function.Consumer;
 
 public class AppInfo implements DexDefinitionSupplier {
@@ -120,7 +121,7 @@
     return app.classes();
   }
 
-  public Iterable<DexProgramClass> classesWithDeterministicOrder() {
+  public List<DexProgramClass> classesWithDeterministicOrder() {
     assert checkIfObsolete();
     return app.classesWithDeterministicOrder();
   }
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 877f2ba..37f45c6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.CallSiteOptimizationInfoPropagator;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
+import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
 import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
@@ -69,6 +70,9 @@
   // Desugared library prefix rewriter.
   public final PrefixRewritingMapper rewritePrefix;
 
+  // Modeling.
+  private final LibraryMethodSideEffectModelCollection libraryMethodSideEffectModelCollection;
+
   // Optimizations.
   private final CallSiteOptimizationInfoPropagator callSiteOptimizationInfoPropagator;
   private final LibraryMemberOptimizer libraryMemberOptimizer;
@@ -108,6 +112,8 @@
     }
 
     this.libraryMemberOptimizer = new LibraryMemberOptimizer(this);
+    this.libraryMethodSideEffectModelCollection =
+        new LibraryMethodSideEffectModelCollection(dexItemFactory());
 
     if (enableWholeProgramOptimizations() && options().protoShrinking().isProtoShrinkingEnabled()) {
       this.protoShrinker = new ProtoShrinker(withLiveness());
@@ -287,6 +293,10 @@
     return libraryMemberOptimizer;
   }
 
+  public LibraryMethodSideEffectModelCollection getLibraryMethodSideEffectModelCollection() {
+    return libraryMethodSideEffectModelCollection;
+  }
+
   public ProtoShrinker protoShrinker() {
     return protoShrinker;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
index ae54120..bae65e8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
@@ -5,11 +5,12 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
-import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -77,23 +78,11 @@
   private void recordOriginalTypeNames(
       DexProgramClass clazz, AppView<? extends AppInfoWithClassHierarchy> appView) {
     DexType type = clazz.getType();
-    VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
-    if (verticallyMergedClasses != null && verticallyMergedClasses.hasBeenMergedIntoSubtype(type)) {
-      return;
-    }
 
-    DexType original = appView.graphLens().getOriginalType(type);
-    if (verticallyMergedClasses != null) {
-      List<DexType> sources = verticallyMergedClasses.getSourcesFor(type);
-      if (!sources.isEmpty()) {
-        renamedTypeNames.put(original, type);
-        sources.forEach(source -> renamedTypeNames.put(source, type));
-        return;
-      }
-    }
-
-    if (original != type) {
-      renamedTypeNames.put(original, type);
+    List<DexType> originalTypes = Lists.newArrayList(appView.graphLens().getOriginalTypes(type));
+    boolean isIdentity = originalTypes.size() == 1 && originalTypes.get(0) == type;
+    if (!isIdentity) {
+      originalTypes.forEach(originalType -> renamedTypeNames.put(originalType, type));
     }
   }
 
@@ -112,6 +101,15 @@
   }
 
   @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    Set<DexType> originalTypes = renamedTypeNames.getKeys(type);
+    if (originalTypes == null) {
+      return ImmutableList.of(type);
+    }
+    return originalTypes;
+  }
+
+  @Override
   public DexField getOriginalFieldSignature(DexField field) {
     return originalFieldSignatures.getOrDefault(field, field);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 516edc0..abb91ed 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -103,7 +103,7 @@
     return box.getClasses();
   }
 
-  public Iterable<DexProgramClass> classesWithDeterministicOrder() {
+  public List<DexProgramClass> classesWithDeterministicOrder() {
     List<DexProgramClass> classes = new ArrayList<>(programClasses());
     // We never actually sort by anything but the DexType, this is just here in case we ever change
     // that.
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
index 115bf84..0a17134 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
@@ -12,11 +12,15 @@
     assert holder.isProgramClass() == (this instanceof ProgramMethod);
   }
 
+  public static ProgramMethod asProgramMethodOrNull(DexClassAndMethod method) {
+    return method != null ? method.asProgramMethod() : null;
+  }
+
   public static DexClassAndMethod create(DexClass holder, DexEncodedMethod method) {
     if (holder.isProgramClass()) {
       return new ProgramMethod(holder.asProgramClass(), method);
     } else if (holder.isLibraryClass()) {
-      return new DexClassAndMethod(holder, method);
+      return new LibraryMethod(holder.asLibraryClass(), method);
     } else {
       assert holder.isClasspathClass();
       return new ClasspathMethod(holder.asClasspathClass(), method);
@@ -46,6 +50,14 @@
     return null;
   }
 
+  public boolean isLibraryMethod() {
+    return false;
+  }
+
+  public LibraryMethod asLibraryMethod() {
+    return null;
+  }
+
   public boolean isProgramMethod() {
     return false;
   }
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 ea4103f..5a1e76d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -425,6 +425,15 @@
     return asProgramMethod(definitions);
   }
 
+  public DexClassAndMethod asDexClassAndMethod(DexDefinitionSupplier definitions) {
+    assert method.holder.isClassType();
+    DexClass clazz = definitions.definitionForHolder(method);
+    if (clazz != null) {
+      return DexClassAndMethod.create(clazz, this);
+    }
+    return null;
+  }
+
   public ProgramMethod asProgramMethod(DexDefinitionSupplier definitions) {
     assert method.holder.isClassType();
     DexProgramClass clazz = asProgramClassOrNull(definitions.definitionForHolder(method));
@@ -434,6 +443,11 @@
     return null;
   }
 
+  public static DexClassAndMethod asDexClassAndMethodOrNull(
+      DexEncodedMethod method, DexDefinitionSupplier definitions) {
+    return method != null ? method.asDexClassAndMethod(definitions) : null;
+  }
+
   public static ProgramMethod asProgramMethodOrNull(
       DexEncodedMethod method, DexDefinitionSupplier definitions) {
     return method != null ? method.asProgramMethod(definitions) : null;
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 cf30eed..b192017 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.analysis.type.ClassTypeElement.computeLeastUpperBoundOfInterfaces;
 import static com.android.tools.r8.ir.optimize.ServiceLoaderRewriter.SERVICE_LOADER_CLASS_NAME;
-import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.Marker;
@@ -31,7 +30,6 @@
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.LRUCacheTable;
-import com.android.tools.r8.utils.Pair;
 import com.google.common.base.Strings;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -39,7 +37,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -57,7 +54,6 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 public class DexItemFactory {
 
@@ -109,8 +105,6 @@
 
   public DexItemFactory() {
     this.kotlin = new Kotlin(this);
-    assert libraryMethodsWithReturnValueDependingOnlyOnArguments.stream()
-        .allMatch(libraryMethodsWithoutSideEffects::containsKey);
   }
 
   public static boolean isInternalSentinel(DexItem item) {
@@ -604,7 +598,7 @@
   }
 
   // Boxed Boxed#valueOf(Primitive), e.g., Boolean Boolean#valueOf(B)
-  private Set<DexMethod> boxedValueOfMethods() {
+  public Set<DexMethod> boxedValueOfMethods() {
     return primitiveToBoxed.entrySet().stream()
         .map(
             entry -> {
@@ -686,31 +680,6 @@
           objectsMethods.requireNonNullWithMessageSupplier,
           stringMembers.valueOf);
 
-  // We assume library methods listed here are `public`, i.e., free from visibility side effects.
-  // If not, that library method should not be added here because it literally has side effects.
-  public Map<DexMethod, Predicate<InvokeMethod>> libraryMethodsWithoutSideEffects =
-      Streams.<Pair<DexMethod, Predicate<InvokeMethod>>>concat(
-              Stream.of(new Pair<>(enumMembers.constructor, alwaysTrue())),
-              Stream.of(new Pair<>(npeMethods.init, alwaysTrue())),
-              Stream.of(new Pair<>(npeMethods.initWithMessage, alwaysTrue())),
-              Stream.of(new Pair<>(objectMembers.constructor, alwaysTrue())),
-              Stream.of(new Pair<>(objectMembers.getClass, alwaysTrue())),
-              Stream.of(new Pair<>(stringMembers.hashCode, alwaysTrue())),
-              mapToPredicate(classMethods.getNames, alwaysTrue()),
-              mapToPredicate(
-                  stringBufferMethods.constructorMethods,
-                  stringBufferMethods::constructorInvokeIsSideEffectFree),
-              mapToPredicate(
-                  stringBuilderMethods.constructorMethods,
-                  stringBuilderMethods::constructorInvokeIsSideEffectFree),
-              mapToPredicate(boxedValueOfMethods(), alwaysTrue()))
-          .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
-
-  private static Stream<Pair<DexMethod, Predicate<InvokeMethod>>> mapToPredicate(
-      Set<DexMethod> methods, Predicate<InvokeMethod> predicate) {
-    return methods.stream().map(method -> new Pair<>(method, predicate));
-  }
-
   // TODO(b/119596718): More idempotent methods? Any singleton accessors? E.g.,
   // java.util.Calendar#getInstance(...) // 4 variants
   // java.util.Locale#getDefault() // returns JVM default locale.
@@ -1130,7 +1099,10 @@
     public final DexField clinitField = createField(objectType, intType, "$r8$clinit");
 
     public final DexMethod clone;
+    public final DexMethod equals =
+        createMethod(objectType, createProto(booleanType, objectType), "equals");
     public final DexMethod getClass;
+    public final DexMethod hashCode = createMethod(objectType, createProto(intType), "hashCode");
     public final DexMethod constructor;
     public final DexMethod finalize;
     public final DexMethod toString;
@@ -1199,7 +1171,7 @@
     public final DexMethod getDeclaredMethod;
     public final DexMethod newInstance;
     private final Set<DexMethod> getMembers;
-    private final Set<DexMethod> getNames;
+    public final Set<DexMethod> getNames;
 
     private ClassMethods() {
       desiredAssertionStatus = createMethod(classDescriptor,
@@ -1561,8 +1533,6 @@
     public final DexMethod toString;
 
     private final Set<DexMethod> appendMethods;
-    private final Set<DexMethod> constructorMethods;
-
     private StringBuildingMethods(DexType receiver) {
       DexString append = createString("append");
 
@@ -1611,6 +1581,8 @@
               charSequenceConstructor, defaultConstructor, intConstructor, stringConstructor);
     }
 
+    public final Set<DexMethod> constructorMethods;
+
     public boolean isAppendMethod(DexMethod method) {
       return appendMethods.contains(method);
     }
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 a0d4cbe..74b0fff 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -37,6 +37,10 @@
     return name;
   }
 
+  public DexType getParameter(int index) {
+    return proto.getParameter(index);
+  }
+
   public DexTypeList getParameters() {
     return proto.parameters;
   }
@@ -219,15 +223,22 @@
 
   @Override
   public String toSourceString() {
-    return toSourceString(true);
+    return toSourceString(true, true);
   }
 
   public String toSourceStringWithoutHolder() {
-    return toSourceString(false);
+    return toSourceString(false, true);
   }
 
-  private String toSourceString(boolean includeHolder) {
-    StringBuilder builder = new StringBuilder(proto.returnType.toSourceString()).append(" ");
+  public String toSourceStringWithoutHolderAndReturnType() {
+    return toSourceString(false, false);
+  }
+
+  private String toSourceString(boolean includeHolder, boolean includeReturnType) {
+    StringBuilder builder = new StringBuilder();
+    if (includeReturnType) {
+      builder.append(getReturnType().toSourceString()).append(" ");
+    }
     if (includeHolder) {
       builder.append(holder.toSourceString()).append(".");
     }
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 ac9b854..eec753e 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ir.desugar.InterfaceProcessor.InterfaceProcessorNestedGraphLens;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 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.ProgramMethodSet;
 import com.google.common.collect.BiMap;
@@ -273,6 +274,8 @@
 
   public abstract DexType getOriginalType(DexType type);
 
+  public abstract Iterable<DexType> getOriginalTypes(DexType type);
+
   public abstract DexField getOriginalFieldSignature(DexField field);
 
   public abstract DexMethod getOriginalMethodSignature(DexMethod method);
@@ -499,15 +502,16 @@
     return result;
   }
 
-  public DexReference rewriteReference(DexReference reference) {
+  @SuppressWarnings("unchecked")
+  public <T extends DexReference> T rewriteReference(T reference) {
     if (reference.isDexField()) {
-      return getRenamedFieldSignature(reference.asDexField());
+      return (T) getRenamedFieldSignature(reference.asDexField());
     }
     if (reference.isDexMethod()) {
-      return getRenamedMethodSignature(reference.asDexMethod());
+      return (T) getRenamedMethodSignature(reference.asDexMethod());
     }
     assert reference.isDexType();
-    return lookupType(reference.asDexType());
+    return (T) lookupType(reference.asDexType());
   }
 
   public Set<DexReference> rewriteReferences(Set<DexReference> references) {
@@ -518,8 +522,8 @@
     return result;
   }
 
-  public <T> ImmutableMap<DexReference, T> rewriteReferenceKeys(Map<DexReference, T> map) {
-    ImmutableMap.Builder<DexReference, T> builder = ImmutableMap.builder();
+  public <R extends DexReference, T> ImmutableMap<R, T> rewriteReferenceKeys(Map<R, T> map) {
+    ImmutableMap.Builder<R, T> builder = ImmutableMap.builder();
     map.forEach((reference, value) -> builder.put(rewriteReference(reference), value));
     return builder.build();
   }
@@ -761,6 +765,11 @@
     }
 
     @Override
+    public Iterable<DexType> getOriginalTypes(DexType type) {
+      return IterableUtils.singleton(type);
+    }
+
+    @Override
     public DexField getOriginalFieldSignature(DexField field) {
       return field;
     }
@@ -845,6 +854,11 @@
     }
 
     @Override
+    public Iterable<DexType> getOriginalTypes(DexType type) {
+      return getPrevious().getOriginalTypes(type);
+    }
+
+    @Override
     public DexField getOriginalFieldSignature(DexField field) {
       return getPrevious().getOriginalFieldSignature(field);
     }
@@ -965,9 +979,22 @@
       return new Builder();
     }
 
+    protected DexType internalGetOriginalType(DexType previous) {
+      return previous;
+    }
+
+    protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
+      return IterableUtils.singleton(internalGetOriginalType(previous));
+    }
+
     @Override
     public DexType getOriginalType(DexType type) {
-      return getPrevious().getOriginalType(type);
+      return getPrevious().getOriginalType(internalGetOriginalType(type));
+    }
+
+    @Override
+    public Iterable<DexType> getOriginalTypes(DexType type) {
+      return IterableUtils.flatMap(internalGetOriginalTypes(type), getPrevious()::getOriginalTypes);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/LibraryMethod.java b/src/main/java/com/android/tools/r8/graph/LibraryMethod.java
new file mode 100644
index 0000000..c06ebab
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/LibraryMethod.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.graph;
+
+/** Type representing a method definition from the library and its holder. */
+public final class LibraryMethod extends DexClassAndMethod {
+
+  public LibraryMethod(DexLibraryClass holder, DexEncodedMethod method) {
+    super(holder, method);
+  }
+
+  @Override
+  public boolean isLibraryMethod() {
+    return true;
+  }
+
+  @Override
+  public LibraryMethod asLibraryMethod() {
+    return this;
+  }
+
+  @Override
+  public DexLibraryClass getHolder() {
+    DexClass holder = super.getHolder();
+    assert holder.isLibraryClass();
+    return holder.asLibraryClass();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
index 949fd47..7c38464 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodArrayBacking.java
@@ -95,6 +95,11 @@
   }
 
   @Override
+  void clearDirectMethods() {
+    directMethods = DexEncodedMethod.EMPTY_ARRAY;
+  }
+
+  @Override
   DexEncodedMethod removeMethod(DexMethod method) {
     DexEncodedMethod removedDirectMethod =
         removeMethodHelper(
@@ -175,6 +180,11 @@
   }
 
   @Override
+  void clearVirtualMethods() {
+    virtualMethods = DexEncodedMethod.EMPTY_ARRAY;
+  }
+
+  @Override
   void setVirtualMethods(DexEncodedMethod[] methods) {
     virtualMethods = MoreObjects.firstNonNull(methods, DexEncodedMethod.EMPTY_ARRAY);
     assert verifyNoDuplicateMethods();
@@ -344,4 +354,26 @@
       }
     }
   }
+
+  @Override
+  public void replaceAllDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    DexEncodedMethod[] oldMethods = directMethods;
+    clearDirectMethods();
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length];
+    for (int i = 0; i < oldMethods.length; i++) {
+      newMethods[i] = replacement.apply(oldMethods[i]);
+    }
+    directMethods = newMethods;
+  }
+
+  @Override
+  public void replaceAllVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    DexEncodedMethod[] oldMethods = virtualMethods;
+    clearVirtualMethods();
+    DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length];
+    for (int i = 0; i < oldMethods.length; i++) {
+      newMethods[i] = replacement.apply(oldMethods[i]);
+    }
+    virtualMethods = newMethods;
+  }
 }
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 312cec2..b14ff47 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -223,14 +223,24 @@
     backing.replaceMethods(replacement);
   }
 
+  public void replaceDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    resetDirectMethodCaches();
+    backing.replaceDirectMethods(replacement);
+  }
+
   public void replaceVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
     resetVirtualMethodCaches();
     backing.replaceVirtualMethods(replacement);
   }
 
-  public void replaceDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+  public void replaceAllDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
     resetDirectMethodCaches();
-    backing.replaceDirectMethods(replacement);
+    backing.replaceAllDirectMethods(replacement);
+  }
+
+  public void replaceAllVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    resetVirtualMethodCaches();
+    backing.replaceAllVirtualMethods(replacement);
   }
 
   /**
@@ -252,6 +262,11 @@
     backing.addDirectMethods(methods);
   }
 
+  public void clearDirectMethods() {
+    resetDirectMethodCaches();
+    backing.clearDirectMethods();
+  }
+
   public DexEncodedMethod removeMethod(DexMethod method) {
     DexEncodedMethod removed = backing.removeMethod(method);
     if (removed != null) {
@@ -283,6 +298,11 @@
     backing.addVirtualMethods(methods);
   }
 
+  public void clearVirtualMethods() {
+    resetVirtualMethodCaches();
+    backing.clearVirtualMethods();
+  }
+
   public void setVirtualMethods(DexEncodedMethod[] methods) {
     assert verifyCorrectnessOfMethodHolders(methods);
     resetVirtualMethodCaches();
diff --git a/src/main/java/com/android/tools/r8/graph/MethodCollectionBacking.java b/src/main/java/com/android/tools/r8/graph/MethodCollectionBacking.java
index 601306c..37a46f0 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollectionBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollectionBacking.java
@@ -94,6 +94,10 @@
 
   // Removal methods.
 
+  abstract void clearDirectMethods();
+
+  abstract void clearVirtualMethods();
+
   abstract DexEncodedMethod removeMethod(DexMethod method);
 
   abstract void removeMethods(Set<DexEncodedMethod> method);
@@ -108,8 +112,12 @@
 
   abstract void replaceDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement);
 
+  abstract void replaceAllDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement);
+
   abstract void replaceVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement);
 
+  abstract void replaceAllVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement);
+
   abstract DexEncodedMethod replaceDirectMethod(
       DexMethod method, Function<DexEncodedMethod, DexEncodedMethod> replacement);
 
diff --git a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
index d877896..2874e03 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
@@ -8,12 +8,14 @@
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Lists;
 import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2ReferenceMap;
 import it.unimi.dsi.fastutil.objects.Object2ReferenceRBTreeMap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Function;
@@ -195,6 +197,16 @@
   }
 
   @Override
+  void clearDirectMethods() {
+    methodMap.values().removeIf(this::belongsToDirectPool);
+  }
+
+  @Override
+  void clearVirtualMethods() {
+    methodMap.values().removeIf(this::belongsToVirtualPool);
+  }
+
+  @Override
   DexEncodedMethod removeMethod(DexMethod method) {
     return methodMap.remove(wrap(method));
   }
@@ -276,6 +288,28 @@
   }
 
   @Override
+  void replaceAllDirectMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    List<DexEncodedMethod> oldMethods = Lists.newArrayList(directMethods());
+    clearDirectMethods();
+    List<DexEncodedMethod> newMethods = new ArrayList<>(oldMethods.size());
+    for (DexEncodedMethod method : oldMethods) {
+      newMethods.add(replacement.apply(method));
+    }
+    addDirectMethods(newMethods);
+  }
+
+  @Override
+  void replaceAllVirtualMethods(Function<DexEncodedMethod, DexEncodedMethod> replacement) {
+    List<DexEncodedMethod> oldMethods = Lists.newArrayList(virtualMethods());
+    clearVirtualMethods();
+    List<DexEncodedMethod> newMethods = new ArrayList<>(oldMethods.size());
+    for (DexEncodedMethod method : oldMethods) {
+      newMethods.add(replacement.apply(method));
+    }
+    addVirtualMethods(newMethods);
+  }
+
+  @Override
   DexEncodedMethod replaceDirectMethod(
       DexMethod method, Function<DexEncodedMethod, DexEncodedMethod> replacement) {
     return replaceMethod(method, replacement, this::belongsToDirectPool);
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
index f76f9e7..ed69ace 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
@@ -7,27 +7,28 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Maps;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 public class VerticallyMergedClasses implements MergedClasses {
 
   private final Map<DexType, DexType> mergedClasses;
-  private final Map<DexType, List<DexType>> sources;
+  private final Map<DexType, Set<DexType>> mergedClassesInverse;
 
-  public VerticallyMergedClasses(Map<DexType, DexType> mergedClasses) {
-    Map<DexType, List<DexType>> sources = Maps.newIdentityHashMap();
-    mergedClasses.forEach(
-        (source, target) -> sources.computeIfAbsent(target, key -> new ArrayList<>()).add(source));
+  public VerticallyMergedClasses(
+      Map<DexType, DexType> mergedClasses, Map<DexType, Set<DexType>> mergedClassesInverse) {
     this.mergedClasses = mergedClasses;
-    this.sources = sources;
+    this.mergedClassesInverse = mergedClassesInverse;
   }
 
-  public List<DexType> getSourcesFor(DexType type) {
-    return sources.getOrDefault(type, ImmutableList.of());
+  public Map<DexType, DexType> getForwardMap() {
+    return mergedClasses;
+  }
+
+  public Collection<DexType> getSourcesFor(DexType type) {
+    return mergedClassesInverse.getOrDefault(type, Collections.emptySet());
   }
 
   public DexType getTargetFor(DexType type) {
@@ -45,7 +46,7 @@
 
   @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
-    for (List<DexType> sourcesForTarget : sources.values()) {
+    for (Collection<DexType> sourcesForTarget : mergedClassesInverse.values()) {
       for (DexType source : sourcesForTarget) {
         assert appView.appInfo().wasPruned(source)
             : "Expected vertically merged class `" + source.toSourceString() + "` to be absent";
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 cfd5182..a6003c4 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -28,8 +28,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.IdentityHashMap;
+import java.util.Comparator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -41,7 +42,7 @@
   public static final String CLASS_ID_FIELD_NAME = "$r8$classId";
 
   private final DexProgramClass target;
-  private final Collection<DexProgramClass> toMergeGroup;
+  private final List<DexProgramClass> toMergeGroup;
   private final DexItemFactory dexItemFactory;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
   private final HorizontallyMergedClasses.Builder mergedClassesBuilder;
@@ -58,7 +59,7 @@
       HorizontallyMergedClasses.Builder mergedClassesBuilder,
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
       DexProgramClass target,
-      Collection<DexProgramClass> toMergeGroup,
+      List<DexProgramClass> toMergeGroup,
       DexField classIdField,
       Collection<VirtualMethodMerger> virtualMethodMergers,
       Collection<ConstructorMerger> constructorMergers) {
@@ -174,9 +175,9 @@
 
   public static class Builder {
     private final DexProgramClass target;
-    private final Collection<DexProgramClass> toMergeGroup = new ArrayList<>();
+    private final List<DexProgramClass> toMergeGroup = new ArrayList<>();
     private final Map<DexProto, ConstructorMerger.Builder> constructorMergerBuilders =
-        new IdentityHashMap<>();
+        new LinkedHashMap<>();
     private final Map<Wrapper<DexMethod>, VirtualMethodMerger.Builder> virtualMethodMergerBuilders =
         new LinkedHashMap<>();
 
@@ -191,7 +192,7 @@
       return this;
     }
 
-    public Builder addClassesToMerge(Collection<DexProgramClass> toMerge) {
+    public Builder addClassesToMerge(List<DexProgramClass> toMerge) {
       toMerge.forEach(this::mergeClass);
       return this;
     }
@@ -237,18 +238,26 @@
       DexField classIdField =
           dexItemFactory.createField(target.type, dexItemFactory.intType, CLASS_ID_FIELD_NAME);
 
-      Collection<VirtualMethodMerger> virtualMethodMergers =
+      List<VirtualMethodMerger> virtualMethodMergers =
           new ArrayList<>(virtualMethodMergerBuilders.size());
       for (VirtualMethodMerger.Builder builder : virtualMethodMergerBuilders.values()) {
         virtualMethodMergers.add(builder.build(appView, target, classIdField));
       }
+      // Try and merge the functions with the most arguments first, to avoid using synthetic
+      // arguments if possible.
+      virtualMethodMergers.sort(Comparator.comparing(VirtualMethodMerger::getArity).reversed());
 
-      Collection<ConstructorMerger> constructorMergers =
+      List<ConstructorMerger> constructorMergers =
           new ArrayList<>(constructorMergerBuilders.size());
       for (ConstructorMerger.Builder builder : constructorMergerBuilders.values()) {
         constructorMergers.add(builder.build(appView, target, classIdField));
       }
 
+      // Try and merge the functions with the most arguments first, to avoid using synthetic
+      // arguments if possible.
+      virtualMethodMergers.sort(Comparator.comparing(VirtualMethodMerger::getArity).reversed());
+      constructorMergers.sort(Comparator.comparing(ConstructorMerger::getArity).reversed());
+
       return new ClassMerger(
           appView,
           lensBuilder,
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 9f201bf..60ff636b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -55,6 +55,10 @@
     this.dexItemFactory = appView.dexItemFactory();
   }
 
+  public int getArity() {
+    return constructors.iterator().next().getReference().getArity();
+  }
+
   public static class Builder {
     private final Collection<DexEncodedMethod> constructors;
 
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 c9a40d7..c8a1c16 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
 import com.android.tools.r8.horizontalclassmerging.policies.DontInlinePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeIntoLessVisible;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
@@ -40,7 +41,7 @@
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -76,6 +77,7 @@
             new NotEntryPoint(appView.dexItemFactory()),
             new DontInlinePolicy(appView, mainDexTracingResult),
             new PreventMergeIntoMainDex(appView, mainDexTracingResult),
+            new AllInstantiatedOrUninstantiated(appView),
             new SameParentClass(),
             new SameNestHost(),
             new PreventChangingVisibility(),
@@ -93,7 +95,7 @@
 
   // TODO(b/165577835): replace Collection<DexProgramClass> with MergeGroup
   public HorizontalClassMergerGraphLens run(DirectMappedDexApplication.Builder appBuilder) {
-    Map<FieldMultiset, Collection<DexProgramClass>> classes = new HashMap<>();
+    Map<FieldMultiset, List<DexProgramClass>> classes = new LinkedHashMap<>();
 
     // Group classes by same field signature using the hash map.
     for (DexProgramClass clazz : appView.appInfo().app().classesWithDeterministicOrder()) {
@@ -101,7 +103,7 @@
     }
 
     // Run the policies on all collected classes to produce a final grouping.
-    Collection<Collection<DexProgramClass>> groups = policyExecutor.run(classes.values());
+    Collection<List<DexProgramClass>> groups = policyExecutor.run(classes.values());
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
       appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty());
@@ -116,7 +118,7 @@
         new FieldAccessInfoCollectionModifier.Builder();
 
     // Set up a class merger for each group.
-    Collection<ClassMerger> classMergers =
+    List<ClassMerger> classMergers =
         initializeClassMergers(
             mergedClassesBuilder, lensBuilder, fieldAccessChangesBuilder, groups);
     Iterable<DexProgramClass> allMergeClasses =
@@ -138,15 +140,15 @@
    * Prepare horizontal class merging by determining which virtual methods and constructors need to
    * be merged and how the merging should be performed.
    */
-  private Collection<ClassMerger> initializeClassMergers(
+  private List<ClassMerger> initializeClassMergers(
       HorizontallyMergedClasses.Builder mergedClassesBuilder,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
-      Collection<Collection<DexProgramClass>> groups) {
-    Collection<ClassMerger> classMergers = new ArrayList<>();
+      Collection<List<DexProgramClass>> groups) {
+    List<ClassMerger> classMergers = new ArrayList<>();
 
     // TODO(b/166577694): Replace Collection<DexProgramClass> with MergeGroup
-    for (Collection<DexProgramClass> group : groups) {
+    for (List<DexProgramClass> group : groups) {
       assert !group.isEmpty();
 
       DexProgramClass target = group.stream().findFirst().get();
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 5f5a62b..867725b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -7,9 +7,11 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
+import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import java.util.ArrayList;
@@ -24,10 +26,11 @@
   private final AppView<?> appView;
   private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters;
   private final Map<DexMethod, DexMethod> originalConstructorSignatures;
+  private final HorizontallyMergedClasses mergedClasses;
 
   private HorizontalClassMergerGraphLens(
       AppView<?> appView,
-      HorizontallyMergedClasses horizontallyMergedClasses,
+      HorizontallyMergedClasses mergedClasses,
       Map<DexMethod, List<ExtraParameter>> methodExtraParameters,
       Map<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
@@ -36,7 +39,7 @@
       Map<DexMethod, DexMethod> originalConstructorSignatures,
       GraphLens previousLens) {
     super(
-        horizontallyMergedClasses.getForwardMap(),
+        mergedClasses.getForwardMap(),
         methodMap,
         fieldMap,
         originalFieldSignatures,
@@ -46,6 +49,12 @@
     this.appView = appView;
     this.methodExtraParameters = methodExtraParameters;
     this.originalConstructorSignatures = originalConstructorSignatures;
+    this.mergedClasses = mergedClasses;
+  }
+
+  @Override
+  protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
+    return IterableUtils.prependSingleton(previous, mergedClasses.getSourcesFor(previous));
   }
 
   @Override
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 653e31a..080102a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -34,6 +34,10 @@
     return mergedClasses.keySet();
   }
 
+  public Set<DexType> getSourcesFor(DexType type) {
+    return mergedClasses.getKeys(type);
+  }
+
   public boolean hasBeenMergedIntoDifferentType(DexType type) {
     return mergedClasses.hasKey(type);
   }
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 829c1f3..2c8f10a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -7,6 +7,7 @@
 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 {
 
@@ -18,7 +19,7 @@
   /**
    * Remove all groups containing no or only a single class, as there is no point in merging these.
    */
-  protected void removeTrivialGroups(Collection<Collection<DexProgramClass>> groups) {
+  protected void removeTrivialGroups(Collection<List<DexProgramClass>> groups) {
     assert !(groups instanceof ArrayList);
     groups.removeIf(this::isTrivial);
   }
@@ -31,5 +32,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<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group);
+  public abstract Collection<List<DexProgramClass>> apply(List<DexProgramClass> 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 0a7e988..bf460d9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassSameReferencePolicy.java
@@ -6,14 +6,15 @@
 
 import com.android.tools.r8.graph.DexProgramClass;
 import java.util.Collection;
-import java.util.IdentityHashMap;
+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<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
-    Map<T, Collection<DexProgramClass>> groups = new IdentityHashMap<>();
+  public final Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+    Map<T, List<DexProgramClass>> groups = new LinkedHashMap<>();
     for (DexProgramClass clazz : group) {
       groups.computeIfAbsent(getMergeKey(clazz), ignore -> new LinkedList<>()).add(clazz);
     }
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 7ffaa1b..cddca41 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.DexProgramClass;
 import java.util.Collection;
+import java.util.List;
 
 public abstract class PolicyExecutor {
   protected final Collection<Policy> policies;
@@ -19,6 +20,5 @@
    * policies registered to this policy executor on the class groups yielding a new collection of
    * class groups.
    */
-  public abstract Collection<Collection<DexProgramClass>> run(
-      Collection<Collection<DexProgramClass>> classes);
+  public abstract Collection<List<DexProgramClass>> run(Collection<List<DexProgramClass>> classes);
 }
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 6192da8..a5f26d8 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -8,6 +8,7 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.stream.Collectors;
 
 /**
@@ -21,9 +22,9 @@
   }
 
   // TODO(b/165506334): if performing mutable operation ensure that linked lists are used
-  private LinkedList<Collection<DexProgramClass>> applySingleClassPolicy(
-      SingleClassPolicy policy, LinkedList<Collection<DexProgramClass>> groups) {
-    Iterator<Collection<DexProgramClass>> i = groups.iterator();
+  private LinkedList<List<DexProgramClass>> applySingleClassPolicy(
+      SingleClassPolicy policy, LinkedList<List<DexProgramClass>> groups) {
+    Iterator<List<DexProgramClass>> i = groups.iterator();
     while (i.hasNext()) {
       Collection<DexProgramClass> group = i.next();
       int previousNumberOfClasses = group.size();
@@ -36,8 +37,8 @@
     return groups;
   }
 
-  private LinkedList<Collection<DexProgramClass>> applyMultiClassPolicy(
-      MultiClassPolicy policy, LinkedList<Collection<DexProgramClass>> groups) {
+  private LinkedList<List<DexProgramClass>> applyMultiClassPolicy(
+      MultiClassPolicy policy, LinkedList<List<DexProgramClass>> groups) {
     // For each group apply the multi class policy and add all the new groups together.
     return groups.stream()
         .flatMap(group -> policy.apply(group).stream())
@@ -45,12 +46,11 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> run(
-      Collection<Collection<DexProgramClass>> inputGroups) {
-    LinkedList<Collection<DexProgramClass>> linkedGroups;
+  public Collection<List<DexProgramClass>> run(Collection<List<DexProgramClass>> inputGroups) {
+    LinkedList<List<DexProgramClass>> linkedGroups;
 
     if (inputGroups instanceof LinkedList) {
-      linkedGroups = (LinkedList<Collection<DexProgramClass>>) inputGroups;
+      linkedGroups = (LinkedList<List<DexProgramClass>>) inputGroups;
     } else {
       linkedGroups = new LinkedList<>(inputGroups);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SubtypingForrestForClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SubtypingForrestForClasses.java
index 402ddb1..b643225 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SubtypingForrestForClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SubtypingForrestForClasses.java
@@ -11,6 +11,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
 
@@ -32,13 +33,12 @@
   private final AppView<AppInfoWithLiveness> appView;
 
   private final Collection<DexProgramClass> roots = new ArrayList<>();
-  private final Map<DexProgramClass, Collection<DexProgramClass>> subtypeMap =
-      new IdentityHashMap<>();
+  private final Map<DexProgramClass, List<DexProgramClass>> subtypeMap = new IdentityHashMap<>();
 
-  public SubtypingForrestForClasses(AppView<AppInfoWithLiveness> appView) {
+  public SubtypingForrestForClasses(
+      AppView<AppInfoWithLiveness> appView, List<DexProgramClass> classesWithDeterministicOrder) {
     this.appView = appView;
-
-    calculateSubtyping(appView.appInfo().classes());
+    calculateSubtyping(classesWithDeterministicOrder);
   }
 
   private DexProgramClass superClass(DexProgramClass clazz) {
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 b102b69..8cf6265 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -120,11 +120,11 @@
    * </ul>
    */
   public HorizontalClassMergerGraphLens fixupTypeReferences() {
-    Iterable<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
+    List<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
     Iterables.filter(classes, DexProgramClass::isInterface).forEach(this::fixupInterfaceClass);
 
     classes.forEach(this::fixupProgramClassSuperType);
-    SubtypingForrestForClasses subtypingForrest = new SubtypingForrestForClasses(appView);
+    SubtypingForrestForClasses subtypingForrest = new SubtypingForrestForClasses(appView, classes);
     // TODO(b/170078037): parallelize this code segment.
     for (DexProgramClass root : subtypingForrest.getProgramRoots()) {
       subtypingForrest.traverseNodeDepthFirst(
@@ -153,18 +153,18 @@
     Map<Wrapper<DexMethod>, DexString> remappedClassVirtualMethods =
         new HashMap<>(remappedVirtualMethods);
 
-    Set<DexMethod> newDirectMethodReferences = new LinkedHashSet<>();
-    Set<DexMethod> newVirtualMethodReferences = new LinkedHashSet<>();
-
+    Set<DexMethod> newVirtualMethodReferences = Sets.newIdentityHashSet();
     clazz
         .getMethodCollection()
-        .replaceVirtualMethods(
+        .replaceAllVirtualMethods(
             method ->
                 fixupVirtualMethod(
                     remappedClassVirtualMethods, newVirtualMethodReferences, method));
+
+    Set<DexMethod> newDirectMethodReferences = Sets.newIdentityHashSet();
     clazz
         .getMethodCollection()
-        .replaceDirectMethods(method -> fixupDirectMethod(newDirectMethodReferences, method));
+        .replaceAllDirectMethods(method -> fixupDirectMethod(newDirectMethodReferences, method));
 
     fixupFields(clazz.staticFields(), clazz::setStaticField);
     fixupFields(clazz.instanceFields(), clazz::setInstanceField);
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 ec5cb59..14488b6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -90,6 +90,10 @@
     }
   }
 
+  public int getArity() {
+    return methods.iterator().next().getReference().getArity();
+  }
+
   private DexMethod moveMethod(ProgramMethod oldMethod) {
     DexMethod oldMethodReference = oldMethod.getReference();
     DexMethod method =
@@ -171,6 +175,13 @@
     DexMethod templateReference = methods.iterator().next().getReference();
     DexMethod originalMethodReference =
         appView.graphLens().getOriginalMethodSignature(templateReference);
+    DexMethod bridgeMethodReference =
+        dexItemFactory.createFreshMethodName(
+            originalMethodReference.getName().toSourceString() + "$bridge",
+            null,
+            originalMethodReference.proto,
+            originalMethodReference.getHolderType(),
+            tryMethod -> target.lookupMethod(tryMethod) == null);
 
     DexMethod newMethodReference =
         dexItemFactory.createMethod(target.type, templateReference.proto, templateReference.name);
@@ -180,7 +191,7 @@
             classIdField,
             superMethod,
             newMethodReference,
-            originalMethodReference);
+            bridgeMethodReference);
     DexEncodedMethod newMethod =
         new DexEncodedMethod(
             newMethodReference,
@@ -196,7 +207,7 @@
     for (ProgramMethod oldMethod : methods) {
       lensBuilder.moveMethod(oldMethod.getReference(), newMethodReference);
     }
-    lensBuilder.recordExtraOriginalSignature(originalMethodReference, newMethodReference);
+    lensBuilder.recordExtraOriginalSignature(bridgeMethodReference, newMethodReference);
 
     target.addVirtualMethod(newMethod);
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
new file mode 100644
index 0000000..75d61d9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class AllInstantiatedOrUninstantiated extends MultiClassSameReferencePolicy<Boolean> {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public AllInstantiatedOrUninstantiated(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public Boolean getMergeKey(DexProgramClass clazz) {
+    return appView.appInfo().isInstantiatedDirectlyOrIndirectly(clazz);
+  }
+}
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 e579310..4e9b69e 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
@@ -14,7 +14,7 @@
 
 public class DontMergeIntoLessVisible extends MultiClassPolicy {
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
     Iterator<DexProgramClass> iterator = group.iterator();
     while (iterator.hasNext()) {
       DexProgramClass clazz = iterator.next();
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 958e660..d8bebf9 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
@@ -12,6 +12,7 @@
 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;
@@ -25,14 +26,14 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
     // Gather all synchronized classes.
-    Collection<Collection<DexProgramClass>> synchronizedGroups = new LinkedList<>();
+    Collection<List<DexProgramClass>> synchronizedGroups = new LinkedList<>();
     group.removeIf(
         clazz -> {
           boolean synchronizationClass = isSynchronizationClass(clazz);
           if (synchronizationClass) {
-            Collection<DexProgramClass> synchronizedGroup = new LinkedList<>();
+            List<DexProgramClass> synchronizedGroup = new LinkedList<>();
             synchronizedGroup.add(clazz);
             synchronizedGroups.add(synchronizedGroup);
           }
@@ -43,7 +44,7 @@
       return Collections.singletonList(group);
     }
 
-    Iterator<Collection<DexProgramClass>> synchronizedGroupIterator = synchronizedGroups.iterator();
+    Iterator<List<DexProgramClass>> 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/NoKeepRules.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
index 969f43a..4d90a1e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexMember;
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
index 112580c..474fcea 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
@@ -42,7 +42,7 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
     Map<DexProto, Set<DexProgramClass>> overlappingConstructors = new IdentityHashMap<>();
 
     for (DexProgramClass clazz : group) {
@@ -106,12 +106,10 @@
     }
 
     // Map to collection
-    Collection<Collection<DexProgramClass>> newGroups = new ArrayList<>();
+    Collection<List<DexProgramClass>> newGroups = new ArrayList<>();
     for (Set<DexProgramClass> newGroup : groups) {
-      List<DexProgramClass> newGroupList = new ArrayList<>(newGroup);
       newGroups.add(new ArrayList<>(newGroup));
     }
-
     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 3c317eb..8b35a5c 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
@@ -23,10 +23,10 @@
   public PreventChangingVisibility() {}
 
   public static class TargetGroup {
-    private final Collection<DexProgramClass> group = new LinkedList<>();
+    private final List<DexProgramClass> group = new LinkedList<>();
     private final Map<Wrapper<DexMethod>, MethodAccessFlags> methodMap = new HashMap<>();
 
-    public Collection<DexProgramClass> getGroup() {
+    public List<DexProgramClass> getGroup() {
       return group;
     }
 
@@ -53,7 +53,7 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
     List<TargetGroup> groups = new ArrayList<>();
 
     for (DexProgramClass clazz : group) {
@@ -66,7 +66,7 @@
       }
     }
 
-    Collection<Collection<DexProgramClass>> newGroups = new ArrayList<>();
+    Collection<List<DexProgramClass>> newGroups = new ArrayList<>();
     for (TargetGroup newGroup : groups) {
       if (!isTrivial(newGroup.getGroup())) {
         newGroups.add(newGroup.getGroup());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
index f17e923..f8f0a3f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
@@ -30,7 +30,7 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
     List<DexProgramClass> mainDexMembers = new LinkedList<>();
     Iterator<DexProgramClass> iterator = group.iterator();
     while (iterator.hasNext()) {
@@ -41,14 +41,13 @@
       }
     }
 
-    Collection<Collection<DexProgramClass>> newGroups = new LinkedList<>();
+    Collection<List<DexProgramClass>> newGroups = new LinkedList<>();
     if (!isTrivial(mainDexMembers)) {
       newGroups.add(mainDexMembers);
     }
     if (!isTrivial(group)) {
       newGroups.add(group);
     }
-
     return newGroups;
   }
 }
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 4b112b8..bd59a0e 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
@@ -16,6 +16,7 @@
 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,7 +59,7 @@
   /** Sort unrestricted classes into restricted classes if they are in the same package. */
   void tryFindRestrictedPackage(
       LinkedList<DexProgramClass> unrestrictedClasses,
-      Map<String, Collection<DexProgramClass>> restrictedClasses) {
+      Map<String, List<DexProgramClass>> restrictedClasses) {
     Iterator<DexProgramClass> i = unrestrictedClasses.iterator();
     while (i.hasNext()) {
       DexProgramClass clazz = i.next();
@@ -72,8 +73,8 @@
   }
 
   @Override
-  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
-    Map<String, Collection<DexProgramClass>> restrictedClasses = new LinkedHashMap<>();
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+    Map<String, List<DexProgramClass>> restrictedClasses = new LinkedHashMap<>();
     LinkedList<DexProgramClass> unrestrictedClasses = new LinkedList<>();
 
     // Sort all restricted classes into packages.
@@ -92,7 +93,7 @@
 
     // TODO(b/166577694): Add the unrestricted classes to restricted groups, but ensure they aren't
     // the merge target.
-    Collection<Collection<DexProgramClass>> groups = new ArrayList<>(restrictedClasses.size() + 1);
+    Collection<List<DexProgramClass>> 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 416f064..3ee0de7 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
@@ -7,6 +7,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -323,10 +324,11 @@
         return false;
       }
       if (appView.appInfo().hasLiveness()) {
-        DexEncodedMethod singleTarget =
+        DexClassAndMethod singleTarget =
             instruction.lookupSingleTarget(appView.withLiveness(), context);
         if (singleTarget != null) {
-          return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
+          return isTypeInitializedBy(
+              instruction, type, singleTarget.getDefinition(), appView, mode);
         }
       }
       DexMethod method = instruction.getInvokedMethod();
@@ -350,8 +352,9 @@
         // Class initialization may fail with ExceptionInInitializerError.
         return false;
       }
-      DexEncodedMethod method = instruction.lookupSingleTarget(appView, context);
-      return method != null && isTypeInitializedBy(instruction, type, method, appView, mode);
+      DexClassAndMethod method = instruction.lookupSingleTarget(appView, context);
+      return method != null
+          && isTypeInitializedBy(instruction, type, method.getDefinition(), appView, mode);
     }
 
     public static boolean forInvokeSuper(
@@ -374,10 +377,11 @@
         return false;
       }
       if (appView.appInfo().hasLiveness()) {
-        DexEncodedMethod singleTarget =
+        DexClassAndMethod singleTarget =
             instruction.lookupSingleTarget(appView.withLiveness(), context);
         if (singleTarget != null) {
-          return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
+          return isTypeInitializedBy(
+              instruction, type, singleTarget.getDefinition(), appView, mode);
         }
       }
       DexMethod method = instruction.getInvokedMethod();
@@ -418,10 +422,11 @@
         return false;
       }
       if (appView.appInfo().hasLiveness()) {
-        DexEncodedMethod singleTarget =
+        DexClassAndMethod singleTarget =
             instruction.lookupSingleTarget(appView.withLiveness(), context);
         if (singleTarget != null) {
-          return isTypeInitializedBy(instruction, type, singleTarget, appView, mode);
+          return isTypeInitializedBy(
+              instruction, type, singleTarget.getDefinition(), appView, mode);
         }
       }
       DexMethod method = instruction.getInvokedMethod();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java
index 32c1bb1..c2a5b53 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.ir.analysis;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -36,9 +36,10 @@
         return false;
       }
       if (instr.isInvokeMethod()) {
-        DexEncodedMethod target =
+        DexClassAndMethod target =
             instr.asInvokeMethod().lookupSingleTarget(appView, code.context());
-        if (target != null && target.getOptimizationInfo().returnValueOnlyDependsOnArguments()) {
+        if (target != null
+            && target.getDefinition().getOptimizationInfo().returnValueOnlyDependsOnArguments()) {
           continue;
         }
         return 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 b73a496..01ea16b 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
@@ -6,8 +6,8 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -132,13 +132,16 @@
         InvokeMethod invoke = instruction.asInvokeMethod();
         DexMethod method = invoke.getInvokedMethod();
         if (method.holder.isClassType()) {
-          DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+          DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
           if (singleTarget != null) {
-            markInitializedOnNormalExit(singleTarget.holder());
+            markInitializedOnNormalExit(singleTarget.getHolderType());
             markInitializedOnNormalExit(
-                singleTarget.getOptimizationInfo().getInitializedClassesOnNormalExit());
+                singleTarget
+                    .getDefinition()
+                    .getOptimizationInfo()
+                    .getInitializedClassesOnNormalExit());
           } else {
-            markInitializedOnNormalExit(method.holder);
+            markInitializedOnNormalExit(method.getHolderType());
           }
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
index ea4040c..2fb3ef7 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/equivalence/BasicBlockBehavioralSubsumption.java
@@ -8,7 +8,7 @@
 import static com.google.common.base.Predicates.or;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstClass;
@@ -160,9 +160,10 @@
     }
     // For constructor calls include field initialization side effects.
     if (instruction.isInvokeConstructor(appView.dexItemFactory())) {
-      DexEncodedMethod singleTarget =
+      DexClassAndMethod singleTarget =
           instruction.asInvokeDirect().lookupSingleTarget(appView, context);
-      return singleTarget != null && !singleTarget.getOptimizationInfo().mayHaveSideEffects();
+      return singleTarget != null
+          && !singleTarget.getDefinition().getOptimizationInfo().mayHaveSideEffects();
     }
     return true;
   }
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 3bc0ebe..d0fd119 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
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -150,7 +151,7 @@
       return;
     }
 
-    DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
     if (singleTarget == null) {
       // We just lost track.
       abstractInstanceFieldValues.remove(clazz);
@@ -158,7 +159,11 @@
     }
 
     InstanceFieldInitializationInfoCollection initializationInfoCollection =
-        singleTarget.getOptimizationInfo().getInstanceInitializerInfo().fieldInitializationInfos();
+        singleTarget
+            .getDefinition()
+            .getOptimizationInfo()
+            .getInstanceInitializerInfo()
+            .fieldInitializationInfos();
 
     // Synchronize on the lattice element (abstractInstanceFieldValuesForClass) in case we process
     // another allocation site of `clazz` concurrently.
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 f508479..d42d5d1 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
@@ -8,8 +8,8 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
@@ -43,14 +43,14 @@
 
   private final InstanceFieldInitializationInfoFactory factory;
 
-  private final DexEncodedMethod parentConstructor;
+  private final DexClassAndMethod parentConstructor;
   private final InvokeDirect parentConstructorCall;
 
   private InstanceFieldValueAnalysis(
       AppView<AppInfoWithLiveness> appView,
       IRCode code,
       OptimizationFeedback feedback,
-      DexEncodedMethod parentConstructor,
+      DexClassAndMethod parentConstructor,
       InvokeDirect parentConstructorCall) {
     super(appView, code, feedback);
     this.factory = appView.instanceFieldInitializationInfoFactory();
@@ -90,7 +90,7 @@
       return EmptyInstanceFieldInitializationInfoCollection.getInstance();
     }
 
-    DexEncodedMethod parentConstructor =
+    DexClassAndMethod parentConstructor =
         parentConstructorCall.lookupSingleTarget(appView, code.context());
     if (parentConstructor == null) {
       return EmptyInstanceFieldInitializationInfoCollection.getInstance();
@@ -167,6 +167,7 @@
     }
     InstanceFieldInitializationInfoCollection infos =
         parentConstructor
+            .getDefinition()
             .getOptimizationInfo()
             .getInstanceInitializerInfo()
             .fieldInitializationInfos();
@@ -271,7 +272,7 @@
     if (field.isFinal()) {
       return true;
     }
-    if (appView.appInfo().isFieldOnlyWrittenInMethod(field, parentConstructor)) {
+    if (appView.appInfo().isFieldOnlyWrittenInMethod(field, parentConstructor.getDefinition())) {
       return true;
     }
     // Otherwise, conservatively return false.
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 ebcdfff..1b4e723 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
@@ -9,8 +9,8 @@
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 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.ir.analysis.type.ClassTypeElement;
@@ -339,13 +339,17 @@
       return ObjectState.empty();
     }
 
-    DexEncodedMethod singleTarget = uniqueConstructorInvoke.lookupSingleTarget(appView, context);
+    DexClassAndMethod singleTarget = uniqueConstructorInvoke.lookupSingleTarget(appView, context);
     if (singleTarget == null) {
       return ObjectState.empty();
     }
 
     InstanceFieldInitializationInfoCollection initializationInfos =
-        singleTarget.getOptimizationInfo().getInstanceInitializerInfo().fieldInitializationInfos();
+        singleTarget
+            .getDefinition()
+            .getOptimizationInfo()
+            .getInstanceInitializerInfo()
+            .fieldInitializationInfos();
     if (initializationInfos.isEmpty()) {
       return ObjectState.empty();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java b/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
index 34ab168..f9a2e25 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
@@ -4,41 +4,39 @@
 
 package com.android.tools.r8.ir.analysis.modeling;
 
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import java.util.function.Predicate;
 
 /** Models if a given library method may cause a program field to be read. */
 public class LibraryMethodReadSetModeling {
 
   public static AbstractFieldSet getModeledReadSetOrUnknown(
-      InvokeMethod invoke, DexItemFactory dexItemFactory) {
+      AppView<?> appView, InvokeMethod invoke) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
 
     // Check if it is a library method that does not have side effects. In that case it is safe to
     // assume that the method does not read any fields, since even if it did, it would not be able
     // to do anything with the values it read (since we will remove such invocations without side
     // effects).
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        dexItemFactory.libraryMethodsWithoutSideEffects.get(invokedMethod);
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(invoke)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(invoke)) {
       return EmptyFieldSet.getInstance();
     }
 
     // Already handled above.
-    assert !dexItemFactory.classMethods.isReflectiveNameLookup(invokedMethod);
+    assert !appView.dexItemFactory().classMethods.isReflectiveNameLookup(invokedMethod);
 
     // Modeling of other library methods.
     DexType holder = invokedMethod.holder;
-    if (holder == dexItemFactory.objectType) {
-      if (invokedMethod == dexItemFactory.objectMembers.constructor) {
-        return EmptyFieldSet.getInstance();
-      }
+    if (holder == appView.dexItemFactory().objectType
+        && invokedMethod == appView.dexItemFactory().objectMembers.constructor) {
+      return EmptyFieldSet.getInstance();
     }
     return UnknownFieldSet.getInstance();
   }
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 2a81647..c61ca0a 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
@@ -3,10 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.asDexClassAndMethodOrNull;
+
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeDirectRange;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -125,22 +128,24 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(
+  public DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
       ClassTypeElement receiverLowerBoundType) {
     DexMethod invokedMethod = getInvokedMethod();
+    DexEncodedMethod result;
     if (appView.appInfo().hasLiveness()) {
       AppInfoWithLiveness appInfo = appView.appInfo().withLiveness();
-      DexEncodedMethod result = appInfo.lookupDirectTarget(invokedMethod, context);
+      result = appInfo.lookupDirectTarget(invokedMethod, context);
       assert verifyD8LookupResult(
           result, appView.appInfo().lookupDirectTargetOnItself(invokedMethod, context));
-      return result;
+    } else {
+      // In D8, we can treat invoke-direct instructions as having a single target if the invoke is
+      // targeting a method in the enclosing class.
+      result = appView.appInfo().lookupDirectTargetOnItself(invokedMethod, context);
     }
-    // In D8, we can treat invoke-direct instructions as having a single target if the invoke is
-    // targeting a method in the enclosing class.
-    return appView.appInfo().lookupDirectTargetOnItself(invokedMethod, context);
+    return asDexClassAndMethodOrNull(result, appView);
   }
 
   @Override
@@ -189,15 +194,19 @@
 
     // Trivial instance initializers do not read any fields.
     if (appView.dexItemFactory().isConstructor(invokedMethod)) {
-      DexEncodedMethod singleTarget = lookupSingleTarget(appView, context);
+      DexClassAndMethod singleTarget = lookupSingleTarget(appView, context);
 
       // If we have a single target in the program, then use the computed initializer info.
       // If we have a single target in the library, then fallthrough to the library modeling below.
-      if (singleTarget != null && singleTarget.isProgramMethod(appView)) {
-        return singleTarget.getOptimizationInfo().getInstanceInitializerInfo().readSet();
+      if (singleTarget != null && singleTarget.isProgramMethod()) {
+        return singleTarget
+            .getDefinition()
+            .getOptimizationInfo()
+            .getInstanceInitializerInfo()
+            .readSet();
       }
     }
 
-    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(this, appView.dexItemFactory());
+    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(appView, this);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
index 83acc1d..6412e17 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
@@ -3,11 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.asDexClassAndMethodOrNull;
 import static com.android.tools.r8.ir.analysis.type.TypeAnalysis.toRefinedReceiverType;
 
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeInterfaceRange;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -95,25 +97,27 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(
+  public DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
       ClassTypeElement receiverLowerBoundType) {
-    if (appView.appInfo().hasLiveness()) {
-      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-      return appViewWithLiveness
-          .appInfo()
-          .lookupSingleVirtualTarget(
-              getInvokedMethod(),
-              context,
-              true,
-              appView,
-              toRefinedReceiverType(
-                  receiverUpperBoundType, getInvokedMethod(), appViewWithLiveness),
-              receiverLowerBoundType);
+    if (!appView.appInfo().hasLiveness()) {
+      return null;
     }
-    return null;
+    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+    DexEncodedMethod result =
+        appViewWithLiveness
+            .appInfo()
+            .lookupSingleVirtualTarget(
+                getInvokedMethod(),
+                context,
+                true,
+                appView,
+                toRefinedReceiverType(
+                    receiverUpperBoundType, getInvokedMethod(), appViewWithLiveness),
+                receiverLowerBoundType);
+    return asDexClassAndMethodOrNull(result, appView);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index c8dd6ec..6daa9a4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -75,11 +76,10 @@
   // In subclasses, e.g., invoke-virtual or invoke-super, use a narrower receiver type by using
   // receiver type and calling context---the holder of the method where the current invocation is.
   // TODO(b/140204899): Refactor lookup methods to be defined in a single place.
-  public abstract DexEncodedMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context);
+  public abstract DexClassAndMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context);
 
   public final ProgramMethod lookupSingleProgramTarget(AppView<?> appView, ProgramMethod context) {
-    DexEncodedMethod singleTarget = lookupSingleTarget(appView, context);
-    return singleTarget != null ? singleTarget.asProgramMethod(appView) : null;
+    return DexClassAndMethod.asProgramMethodOrNull(lookupSingleTarget(appView, context));
   }
 
   // TODO(b/140204899): Refactor lookup methods to be defined in a single place.
@@ -202,16 +202,16 @@
 
   @Override
   public AbstractFieldSet readSet(AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(this, appView.dexItemFactory());
+    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(appView, this);
   }
 
   @Override
   public AbstractValue getAbstractValue(
       AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
     assert hasOutValue();
-    DexEncodedMethod method = lookupSingleTarget(appView, context);
+    DexClassAndMethod method = lookupSingleTarget(appView, context);
     if (method != null) {
-      return method.getOptimizationInfo().getAbstractReturnValue();
+      return method.getDefinition().getOptimizationInfo().getAbstractReturnValue();
     }
     return UnknownValue.getInstance();
   }
@@ -227,9 +227,10 @@
 
   @Override
   public boolean throwsNpeIfValueIsNull(Value value, AppView<?> appView, ProgramMethod context) {
-    DexEncodedMethod singleTarget = lookupSingleTarget(appView, context);
+    DexClassAndMethod singleTarget = lookupSingleTarget(appView, context);
     if (singleTarget != null) {
-      BitSet nonNullParamOrThrow = singleTarget.getOptimizationInfo().getNonNullParamOrThrow();
+      BitSet nonNullParamOrThrow =
+          singleTarget.getDefinition().getOptimizationInfo().getNonNullParamOrThrow();
       if (nonNullParamOrThrow != null) {
         int argumentIndex = inValues.indexOf(value);
         return argumentIndex >= 0 && nonNullParamOrThrow.get(argumentIndex);
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 b006d6a..2aeb9b5 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
@@ -3,10 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
-import static com.android.tools.r8.graph.DexEncodedMethod.asProgramMethodOrNull;
-
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -26,7 +25,6 @@
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.List;
-import java.util.function.Predicate;
 
 public abstract class InvokeMethodWithReceiver extends InvokeMethod {
 
@@ -73,7 +71,7 @@
   }
 
   @Override
-  public final DexEncodedMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
+  public final DexClassAndMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
     TypeElement receiverUpperBoundType = null;
     ClassTypeElement receiverLowerBoundType = null;
     if (appView.enableWholeProgramOptimizations()) {
@@ -84,7 +82,7 @@
     return lookupSingleTarget(appView, context, receiverUpperBoundType, receiverLowerBoundType);
   }
 
-  public abstract DexEncodedMethod lookupSingleTarget(
+  public abstract DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
@@ -95,9 +93,8 @@
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
       ClassTypeElement receiverLowerBoundType) {
-    return asProgramMethodOrNull(
-        lookupSingleTarget(appView, context, receiverUpperBoundType, receiverLowerBoundType),
-        appView);
+    return DexClassAndMethod.asProgramMethodOrNull(
+        lookupSingleTarget(appView, context, receiverUpperBoundType, receiverLowerBoundType));
   }
 
   @Override
@@ -197,9 +194,9 @@
     }
 
     // Check if it is a call to one of library methods that are known to be side-effect free.
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(this)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(this)) {
       return false;
     }
 
@@ -237,18 +234,26 @@
     }
 
     // Find the target and check if the invoke may have side effects.
-    DexEncodedMethod target = lookupSingleTarget(appViewWithLiveness, context);
-    if (target == null) {
+    DexClassAndMethod singleTarget = lookupSingleTarget(appViewWithLiveness, context);
+    if (singleTarget == null) {
       return true;
     }
 
-    // Verify that the target method does not have side-effects.
-    if (appViewWithLiveness.appInfo().noSideEffects.containsKey(target.method)) {
+    if (singleTarget.isLibraryMethod()
+        && appView
+            .getLibraryMethodSideEffectModelCollection()
+            .isSideEffectFree(this, singleTarget.asLibraryMethod())) {
       return false;
     }
 
-    MethodOptimizationInfo optimizationInfo = target.getOptimizationInfo();
-    if (target.isInstanceInitializer()) {
+    // Verify that the target method does not have side-effects.
+    if (appViewWithLiveness.appInfo().noSideEffects.containsKey(singleTarget.getReference())) {
+      return false;
+    }
+
+    DexEncodedMethod singleTargetDefinition = singleTarget.getDefinition();
+    MethodOptimizationInfo optimizationInfo = singleTargetDefinition.getOptimizationInfo();
+    if (singleTargetDefinition.isInstanceInitializer()) {
       InstanceInitializerInfo initializerInfo = optimizationInfo.getInstanceInitializerInfo();
       if (!initializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
         return !isInvokeDirect();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index dbcafdc..425262b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.code.InvokePolymorphicRange;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
@@ -125,7 +125,7 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
+  public DexClassAndMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
     // TODO(herhut): Implement lookup target for invokePolymorphic.
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 696d674..bf3a599 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -3,10 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.asDexClassAndMethodOrNull;
+
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeStaticRange;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -26,7 +29,6 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.Sets;
 import java.util.List;
-import java.util.function.Predicate;
 
 public class InvokeStatic extends InvokeMethod {
 
@@ -107,24 +109,27 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
+  public DexClassAndMethod lookupSingleTarget(AppView<?> appView, ProgramMethod context) {
     DexMethod invokedMethod = getInvokedMethod();
+    DexEncodedMethod result;
     if (appView.appInfo().hasLiveness()) {
       AppInfoWithLiveness appInfo = appView.appInfo().withLiveness();
-      DexEncodedMethod result = appInfo.lookupStaticTarget(invokedMethod, context);
+      result = appInfo.lookupStaticTarget(invokedMethod, context);
       assert verifyD8LookupResult(
           result, appView.appInfo().lookupStaticTargetOnItself(invokedMethod, context));
-      return result;
+    } else {
+      // Allow optimizing static library invokes in D8.
+      DexClass clazz = appView.definitionForHolder(getInvokedMethod());
+      if (clazz != null
+          && (clazz.isLibraryClass() || appView.libraryMethodOptimizer().isModeled(clazz.type))) {
+        result = clazz.lookupMethod(getInvokedMethod());
+      } else {
+        // In D8, we can treat invoke-static instructions as having a single target if the invoke is
+        // targeting a method in the enclosing class.
+        result = appView.appInfo().lookupStaticTargetOnItself(invokedMethod, context);
+      }
     }
-    // Allow optimizing static library invokes in D8.
-    DexClass clazz = appView.definitionForHolder(getInvokedMethod());
-    if (clazz != null
-        && (clazz.isLibraryClass() || appView.libraryMethodOptimizer().isModeled(clazz.type))) {
-      return clazz.lookupMethod(getInvokedMethod());
-    }
-    // In D8, we can treat invoke-static instructions as having a single target if the invoke is
-    // targeting a method in the enclosing class.
-    return appView.appInfo().lookupStaticTargetOnItself(invokedMethod, context);
+    return asDexClassAndMethodOrNull(result, appView);
   }
 
   @Override
@@ -173,9 +178,9 @@
     }
 
     // Check if it is a call to one of library methods that are known to be side-effect free.
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(this)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(this)) {
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
index 34fa59c..8ce0b70 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
@@ -3,9 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.asDexClassAndMethodOrNull;
+
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeSuperRange;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -102,7 +105,7 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(
+  public DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
@@ -111,7 +114,8 @@
       AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
       AppInfoWithLiveness appInfo = appViewWithLiveness.appInfo();
       if (appInfo.isSubtype(context.getHolderType(), getInvokedMethod().holder)) {
-        return appInfo.lookupSuperTarget(getInvokedMethod(), context);
+        DexEncodedMethod result = appInfo.lookupSuperTarget(getInvokedMethod(), context);
+        return asDexClassAndMethodOrNull(result, appView);
       }
     }
     return null;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index e7fb163..d4f737e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -3,12 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.graph.DexEncodedMethod.asDexClassAndMethodOrNull;
 import static com.android.tools.r8.ir.analysis.type.TypeAnalysis.toRefinedReceiverType;
 
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.InvokeVirtualRange;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -96,7 +98,7 @@
   }
 
   @Override
-  public DexEncodedMethod lookupSingleTarget(
+  public DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
@@ -105,38 +107,43 @@
         appView, context, receiverUpperBoundType, receiverLowerBoundType, getInvokedMethod());
   }
 
-  public static DexEncodedMethod lookupSingleTarget(
+  public static DexClassAndMethod lookupSingleTarget(
       AppView<?> appView,
       ProgramMethod context,
       TypeElement receiverUpperBoundType,
       ClassTypeElement receiverLowerBoundType,
       DexMethod method) {
+    DexEncodedMethod result = null;
     if (appView.appInfo().hasLiveness()) {
       AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-      return appViewWithLiveness
-          .appInfo()
-          .lookupSingleVirtualTarget(
-              method,
-              context,
-              false,
-              appView,
-              toRefinedReceiverType(receiverUpperBoundType, method, appViewWithLiveness),
-              receiverLowerBoundType);
-    }
-    // In D8, allow lookupSingleTarget() to be used for finding final library methods. This is used
-    // for library modeling.
-    DexType holder = method.holder;
-    if (holder.isClassType()) {
-      DexClass clazz = appView.definitionFor(holder);
-      if (clazz != null
-          && (clazz.isLibraryClass() || appView.libraryMethodOptimizer().isModeled(clazz.type))) {
-        DexEncodedMethod singleTargetCandidate = clazz.lookupMethod(method);
-        if (singleTargetCandidate != null && (clazz.isFinal() || singleTargetCandidate.isFinal())) {
-          return singleTargetCandidate;
+      result =
+          appViewWithLiveness
+              .appInfo()
+              .lookupSingleVirtualTarget(
+                  method,
+                  context,
+                  false,
+                  appView,
+                  toRefinedReceiverType(receiverUpperBoundType, method, appViewWithLiveness),
+                  receiverLowerBoundType);
+    } else {
+      // In D8, allow lookupSingleTarget() to be used for finding final library methods. This is
+      // used
+      // for library modeling.
+      DexType holder = method.holder;
+      if (holder.isClassType()) {
+        DexClass clazz = appView.definitionFor(holder);
+        if (clazz != null
+            && (clazz.isLibraryClass() || appView.libraryMethodOptimizer().isModeled(clazz.type))) {
+          DexEncodedMethod singleTargetCandidate = clazz.lookupMethod(method);
+          if (singleTargetCandidate != null
+              && (clazz.isFinal() || singleTargetCandidate.isFinal())) {
+            result = singleTargetCandidate;
+          }
         }
       }
     }
-    return null;
+    return asDexClassAndMethodOrNull(result, appView);
   }
 
   @Override
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 c337afe..9356caa 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
@@ -186,6 +186,10 @@
             .containsKey(method.getHolderType())) {
       return false;
     }
+    if (!appView.options().desugaredLibraryConfiguration.supportAllCallbacksFromLibrary
+        && appView.options().isDesugaredLibraryCompilation()) {
+      return false;
+    }
     return overridesLibraryMethod(method);
   }
 
@@ -212,6 +216,9 @@
       if (!dexClass.isLibraryClass() && !appView.options().isDesugaredLibraryCompilation()) {
         continue;
       }
+      if (!shouldGenerateCallbacksForEmulateInterfaceAPIs(dexClass)) {
+        continue;
+      }
       DexEncodedMethod dexEncodedMethod = dexClass.lookupVirtualMethod(method.getReference());
       if (dexEncodedMethod != null) {
         // In this case, the object will be wrapped.
@@ -224,6 +231,16 @@
     return foundOverrideToRewrite;
   }
 
+  private boolean shouldGenerateCallbacksForEmulateInterfaceAPIs(DexClass dexClass) {
+    if (appView.options().desugaredLibraryConfiguration.supportAllCallbacksFromLibrary) {
+      return true;
+    }
+    Map<DexType, DexType> emulateLibraryInterfaces =
+        appView.options().desugaredLibraryConfiguration.getEmulateLibraryInterface();
+    return !(emulateLibraryInterfaces.containsKey(dexClass.type)
+        || emulateLibraryInterfaces.containsValue(dexClass.type));
+  }
+
   private synchronized void registerCallback(ProgramMethod method) {
     // In R8 we should be in the enqueuer, therefore we can duplicate a default method and both
     // methods will be desugared.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
index 542c5d7..9896987 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfiguration.java
@@ -36,12 +36,15 @@
 public class DesugaredLibraryConfiguration {
 
   public static final String FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX = "j$/";
+  public static final boolean FALL_BACK_SUPPORT_ALL_CALLBACKS_FROM_LIBRARY = true;
+
   public static final DesugaredLibraryConfiguration EMPTY_DESUGARED_LIBRARY_CONFIGURATION =
       new DesugaredLibraryConfiguration(
           AndroidApiLevel.B,
           false,
           FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX,
           null,
+          FALL_BACK_SUPPORT_ALL_CALLBACKS_FROM_LIBRARY,
           ImmutableMap.of(),
           ImmutableMap.of(),
           ImmutableMap.of(),
@@ -51,11 +54,18 @@
           ImmutableList.of(),
           ImmutableList.of());
 
-  // TODO(b/158632510): should use DexString, DexType, DexMethod or so on when possible.
   private final AndroidApiLevel requiredCompilationAPILevel;
   private final boolean libraryCompilation;
   private final String synthesizedLibraryClassesPackagePrefix;
   private final String identifier;
+  // Setting supportAllCallbacksFromLibrary reduces the number of generated call-backs,
+  // more specifically:
+  // - no call-back is generated for emulated interface method overrides (forEach, etc.)
+  // - no call-back is generated inside the desugared library itself.
+  // Such setting decreases significantly the desugared library dex file, but virtual calls from
+  // within the library to desugared library classes instances as receiver may be incorrect, for
+  // example the method forEach in Iterable may be executed over a concrete implementation.
+  public final boolean supportAllCallbacksFromLibrary;
   private final Map<String, String> rewritePrefix;
   private final Map<DexType, DexType> emulateLibraryInterface;
   private final Map<DexString, Map<DexType, DexType>> retargetCoreLibMember;
@@ -76,6 +86,7 @@
         true,
         FALL_BACK_SYNTHESIZED_CLASSES_PACKAGE_PREFIX,
         "testingOnlyVersion",
+        FALL_BACK_SUPPORT_ALL_CALLBACKS_FROM_LIBRARY,
         prefix,
         ImmutableMap.of(),
         ImmutableMap.of(),
@@ -95,6 +106,7 @@
       boolean libraryCompilation,
       String packagePrefix,
       String identifier,
+      boolean supportAllCallbacksFromLibrary,
       Map<String, String> rewritePrefix,
       Map<DexType, DexType> emulateLibraryInterface,
       Map<DexString, Map<DexType, DexType>> retargetCoreLibMember,
@@ -107,6 +119,7 @@
     this.libraryCompilation = libraryCompilation;
     this.synthesizedLibraryClassesPackagePrefix = packagePrefix;
     this.identifier = identifier;
+    this.supportAllCallbacksFromLibrary = supportAllCallbacksFromLibrary;
     this.rewritePrefix = rewritePrefix;
     this.emulateLibraryInterface = emulateLibraryInterface;
     this.retargetCoreLibMember = retargetCoreLibMember;
@@ -204,6 +217,7 @@
     private Set<DexType> wrapperConversions = Sets.newIdentityHashSet();
     private List<Pair<DexType, DexString>> dontRewriteInvocation = new ArrayList<>();
     private List<String> extraKeepRules = Collections.emptyList();
+    private boolean supportAllCallbacksFromLibrary = FALL_BACK_SUPPORT_ALL_CALLBACKS_FROM_LIBRARY;
 
     private Builder(DexItemFactory dexItemFactory, Reporter reporter, Origin origin) {
       this.factory = dexItemFactory;
@@ -344,6 +358,10 @@
       return factory.createType(DescriptorUtils.javaTypeToDescriptor(stringClass));
     }
 
+    public void setSupportAllCallbacksFromLibrary(boolean supportAllCallbacksFromLibrary) {
+      this.supportAllCallbacksFromLibrary = supportAllCallbacksFromLibrary;
+    }
+
     public DesugaredLibraryConfiguration build() {
       validate();
       return new DesugaredLibraryConfiguration(
@@ -351,6 +369,7 @@
           libraryCompilation,
           synthesizedLibraryClassesPackagePrefix,
           identifier,
+          supportAllCallbacksFromLibrary,
           ImmutableMap.copyOf(rewritePrefix),
           ImmutableMap.copyOf(emulateLibraryInterface),
           ImmutableMap.copyOf(retargetCoreLibMember),
@@ -373,5 +392,6 @@
                 origin));
       }
     }
+
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
index 7586210..a20c8e0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryConfigurationParser.java
@@ -47,6 +47,7 @@
   static final String DONT_REWRITE_KEY = "dont_rewrite";
   static final String BACKPORT_KEY = "backport";
   static final String SHRINKER_CONFIG_KEY = "shrinker_config";
+  static final String SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY = "support_all_callbacks_from_library";
 
   private final DexItemFactory dexItemFactory;
   private final Reporter reporter;
@@ -149,6 +150,12 @@
       }
       configurationBuilder.setExtraKeepRules(extraKeepRules);
     }
+
+    if (jsonConfig.has(SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY)) {
+      boolean supportAllCallbacksFromLibrary =
+          jsonConfig.get(SUPPORT_ALL_CALLBACKS_FROM_LIBRARY_KEY).getAsBoolean();
+      configurationBuilder.setSupportAllCallbacksFromLibrary(supportAllCallbacksFromLibrary);
+    }
     configurationAmender.accept(configurationBuilder);
     DesugaredLibraryConfiguration config = configurationBuilder.build();
     configurationBuilder = null;
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 4a3ee89..f9985d5 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
@@ -48,6 +48,7 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ObjectUtils;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
@@ -1004,18 +1005,19 @@
     GenericSignature.ClassSignature classSignature = clazz.getClassSignature();
     for (int i = 0; i < clazz.interfaces.size(); i++) {
       DexType itf = clazz.interfaces.values[i];
-      assert emulatedInterfaces.containsKey(itf);
-      List<GenericSignature.FieldTypeSignature> typeArguments;
-      if (classSignature == null) {
-        typeArguments = Collections.emptyList();
-      } else {
-        GenericSignature.ClassTypeSignature classTypeSignature =
-            classSignature.superInterfaceSignatures().get(i);
-        assert itf == classTypeSignature.type();
-        typeArguments = classTypeSignature.typeArguments();
+      if (emulatedInterfaces.containsKey(itf)) {
+        List<GenericSignature.FieldTypeSignature> typeArguments;
+        if (classSignature == null) {
+          typeArguments = Collections.emptyList();
+        } else {
+          GenericSignature.ClassTypeSignature classTypeSignature =
+              classSignature.superInterfaceSignatures().get(i);
+          assert itf == classTypeSignature.type();
+          typeArguments = classTypeSignature.typeArguments();
+        }
+        newInterfaces.add(
+            new GenericSignature.ClassTypeSignature(emulatedInterfaces.get(itf), typeArguments));
       }
-      newInterfaces.add(
-          new GenericSignature.ClassTypeSignature(emulatedInterfaces.get(itf), typeArguments));
     }
     clazz.replaceInterfaces(newInterfaces);
   }
@@ -1118,6 +1120,10 @@
         || missing.isD8R8SynthesizedClassType()
         || isCompanionClassType(missing)
         || emulatedInterfaces.containsValue(missing)
+        || ObjectUtils.getBooleanOrElse(
+            options.getProguardConfiguration(),
+            configuration -> configuration.getDontWarnPatterns().matches(missing),
+            false)
         || options.desugaredLibraryConfiguration.getCustomConversions().containsValue(missing);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 9b8f930..c9e47c2 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -233,7 +233,7 @@
         new DexEncodedMethod(
             mainMethod,
             MethodAccessFlags.fromSharedAccessFlags(
-                Constants.ACC_PUBLIC | Constants.ACC_FINAL | Constants.ACC_SYNTHETIC, false),
+                Constants.ACC_PUBLIC | Constants.ACC_FINAL, false),
             MethodTypeSignature.noSignature(),
             DexAnnotationSet.empty(),
             ParameterAnnotationsList.empty(),
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
index 0008896..d6eae9a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssumeInserter.java
@@ -8,8 +8,8 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
@@ -225,13 +225,13 @@
 
   private boolean computeAssumedValuesFromSingleTarget(
       IRCode code, InvokeMethod invoke, AssumedValues.Builder assumedValuesBuilder) {
-    DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
     if (singleTarget == null) {
       return false;
     }
 
     boolean needsAssumeInstruction = false;
-    MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
+    MethodOptimizationInfo optimizationInfo = singleTarget.getDefinition().getOptimizationInfo();
 
     // Case (2), invocations that are guaranteed to return a non-null value.
     Value outValue = invoke.outValue();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 4c61a1c..61b13cb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -1253,9 +1254,14 @@
         }
 
         // Check if the invoked method is known to return one of its arguments.
-        DexEncodedMethod target = invoke.lookupSingleTarget(appView, code.context());
-        if (target != null && target.getOptimizationInfo().returnsArgument()) {
-          int argumentIndex = target.getOptimizationInfo().getReturnedArgument();
+        DexClassAndMethod target = invoke.lookupSingleTarget(appView, code.context());
+        if (target == null) {
+          continue;
+        }
+
+        MethodOptimizationInfo optimizationInfo = target.getDefinition().getOptimizationInfo();
+        if (optimizationInfo.returnsArgument()) {
+          int argumentIndex = optimizationInfo.getReturnedArgument();
           // Replace the out value of the invoke with the argument and ignore the out value.
           if (argumentIndex >= 0 && checkArgumentType(invoke, argumentIndex)) {
             Value argument = invoke.arguments().get(argumentIndex);
@@ -2878,13 +2884,14 @@
         }
 
         InvokeMethod invoke = instruction.asInvokeMethod();
-        DexEncodedMethod singleTarget =
+        DexClassAndMethod singleTarget =
             invoke.lookupSingleTarget(appView.withLiveness(), code.context());
         if (singleTarget == null) {
           continue;
         }
 
-        MethodOptimizationInfo optimizationInfo = singleTarget.getOptimizationInfo();
+        MethodOptimizationInfo optimizationInfo =
+            singleTarget.getDefinition().getOptimizationInfo();
 
         // If the invoke instruction is a null check, we can remove it.
         boolean isNullCheck = false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 36fba35..912a030 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -11,6 +11,9 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
@@ -147,13 +150,19 @@
           continue;
         }
         SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
+        DexType fieldHolderType = singleFieldValue.getField().getHolderType();
         if (context.getDefinition().isClassInitializer()
-            && context.getHolderType() == singleFieldValue.getField().holder) {
+            && context.getHolderType() == fieldHolderType) {
           // Avoid that canonicalization inserts a read before the unique write in the class
           // initializer, as that would change the program behavior.
           continue;
         }
-        if (current.instructionMayHaveSideEffects(appView, context)) {
+        DexClass fieldHolder = appView.definitionFor(fieldHolderType);
+        DexEncodedField field = singleFieldValue.getField().lookupOnClass(fieldHolder);
+        if (field == null
+            || !field.isEnum()
+            || current.instructionMayHaveSideEffects(appView, context)) {
+          // Only allow canonicalization of enums.
           continue;
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index 4c7570c..d648bc1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -123,19 +123,18 @@
           // Check if the instruction can be rewritten to invoke-super. This allows inlining of the
           // enclosing method into contexts outside the current class.
           if (appView.options().testing.enableInvokeSuperToInvokeVirtualRewriting) {
-            DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+            DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
             if (singleTarget != null) {
-              DexClass holder = appView.definitionForHolder(singleTarget, context);
-              assert holder != null;
               DexMethod invokedMethod = invoke.getInvokedMethod();
-              DexEncodedMethod newSingleTarget =
+              DexClassAndMethod newSingleTarget =
                   InvokeVirtual.lookupSingleTarget(
                       appView,
                       context,
                       invoke.getReceiver().getDynamicUpperBoundType(appView),
                       invoke.getReceiver().getDynamicLowerBoundType(appView),
                       invokedMethod);
-              if (newSingleTarget == singleTarget) {
+              if (newSingleTarget != null
+                  && newSingleTarget.getReference() == singleTarget.getReference()) {
                 it.replaceCurrentInstruction(
                     new InvokeVirtual(invokedMethod, invoke.outValue(), invoke.arguments()));
                 continue;
@@ -173,12 +172,11 @@
           continue;
         }
         InvokeInterface invoke = current.asInvokeInterface();
-        DexEncodedMethod target = invoke.lookupSingleTarget(appView, context);
+        DexClassAndMethod target = invoke.lookupSingleTarget(appView, context);
         if (target == null) {
           continue;
         }
-        DexType holderType = target.holder();
-        DexClass holderClass = appView.definitionFor(holderType);
+        DexClass holderClass = target.getHolder();
         // Make sure we are not landing on another interface, e.g., interface's default method.
         if (holderClass == null || holderClass.isInterface()) {
           continue;
@@ -190,7 +188,7 @@
         }
 
         InvokeVirtual devirtualizedInvoke =
-            new InvokeVirtual(target.method, invoke.outValue(), invoke.inValues());
+            new InvokeVirtual(target.getReference(), invoke.outValue(), invoke.inValues());
         it.replaceCurrentInstruction(devirtualizedInvoke);
         devirtualizedCall.put(invoke, devirtualizedInvoke);
 
@@ -204,11 +202,12 @@
         // CodeRewriter#removeTrivialCheckCastAndInstanceOfInstructions}.
         // a <- check-cast A i  // Otherwise ART verification error.
         // (out <-) invoke-virtual a, ... A#foo
-        if (holderType != invoke.getInvokedMethod().holder) {
+        if (holderClass.getType() != invoke.getInvokedMethod().holder) {
           Value receiver = invoke.getReceiver();
           TypeElement receiverTypeLattice = receiver.getType();
           TypeElement castTypeLattice =
-              TypeElement.fromDexType(holderType, receiverTypeLattice.nullability(), appView);
+              TypeElement.fromDexType(
+                  holderClass.getType(), receiverTypeLattice.nullability(), appView);
           // Avoid adding trivial cast and up-cast.
           // We should not use strictlyLessThan(castType, receiverType), which detects downcast,
           // due to side-casts, e.g., A (unused) < I, B < I, and cast from A to B.
@@ -224,8 +223,8 @@
             // a2 <- check-cast A i  // We should be able to reuse a1 here!
             // invoke-virtual a2, ... A#m2 (from I#m2)
             if (castedReceiverCache.containsKey(receiver)
-                && castedReceiverCache.get(receiver).containsKey(holderType)) {
-              Value cachedReceiver = castedReceiverCache.get(receiver).get(holderType);
+                && castedReceiverCache.get(receiver).containsKey(holderClass.getType())) {
+              Value cachedReceiver = castedReceiverCache.get(receiver).get(holderClass.getType());
               if (dominatorTree.dominatedBy(block, cachedReceiver.definition.getBlock())) {
                 newReceiver = cachedReceiver;
               }
@@ -237,9 +236,9 @@
               // Cache the new receiver with a narrower type to avoid redundant checkcast.
               if (!receiver.hasLocalInfo()) {
                 castedReceiverCache.putIfAbsent(receiver, new IdentityHashMap<>());
-                castedReceiverCache.get(receiver).put(holderType, newReceiver);
+                castedReceiverCache.get(receiver).put(holderClass.getType(), newReceiver);
               }
-              CheckCast checkCast = new CheckCast(newReceiver, receiver, holderType);
+              CheckCast checkCast = new CheckCast(newReceiver, receiver, holderClass.getType());
               checkCast.setPosition(invoke.getPosition());
               newCheckCastInstructions.add(checkCast);
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
index 10d17e7..50443c6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.ir.optimize;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.StringUtils;
@@ -29,7 +30,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Predicate;
 
 /**
  * Canonicalize idempotent function calls.
@@ -154,10 +154,14 @@
           //  return the resolution result such that the call site can perform the accessibility
           //  check, or (iii) always perform the accessibility check such that it can be skipped
           //  at the call site.
-          DexEncodedMethod target = invoke.lookupSingleTarget(appViewWithLiveness, context);
-          if (target == null
-              || target.getOptimizationInfo().mayHaveSideEffects()
-              || !target.getOptimizationInfo().returnValueOnlyDependsOnArguments()) {
+          DexClassAndMethod target = invoke.lookupSingleTarget(appViewWithLiveness, context);
+          if (target == null) {
+            continue;
+          }
+
+          MethodOptimizationInfo optimizationInfo = target.getDefinition().getOptimizationInfo();
+          if (optimizationInfo.mayHaveSideEffects()
+              || !optimizationInfo.returnValueOnlyDependsOnArguments()) {
             continue;
           }
 
@@ -272,12 +276,10 @@
 
   private boolean isIdempotentLibraryMethodInvoke(InvokeMethod invoke) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
-    Predicate<InvokeMethod> noSideEffectPredicate =
-        factory.libraryMethodsWithoutSideEffects.get(invokedMethod);
-    if (noSideEffectPredicate == null || !noSideEffectPredicate.test(invoke)) {
-      return false;
-    }
-    return factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod);
+    return appView
+            .getLibraryMethodSideEffectModelCollection()
+            .isCallToSideEffectFreeFinalMethod(invoke)
+        && factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod);
   }
 
   private static void insertCanonicalizedInvokeWithInValues(
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 96fd58c..621f3b9 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
@@ -8,6 +8,7 @@
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexDefinition;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -103,12 +104,16 @@
         || appView.appInfo().noSideEffects.containsKey(field.field);
   }
 
-  private boolean mayPropagateValueFor(DexEncodedMethod method) {
-    if (method.isProgramMethod(appView)) {
-      return appView.appInfo().mayPropagateValueFor(method.method);
+  private boolean mayPropagateValueFor(DexClassAndMethod method) {
+    if (method.isProgramMethod()) {
+      return appView.appInfo().mayPropagateValueFor(method.getReference());
     }
-    return appView.appInfo().assumedValues.containsKey(method.method)
-        || appView.appInfo().noSideEffects.containsKey(method.method);
+    return appView.appInfo().assumedValues.containsKey(method.getReference())
+        || appView.appInfo().noSideEffects.containsKey(method.getReference());
+  }
+
+  private ProguardMemberRuleLookup lookupMemberRule(DexClassAndMethod method) {
+    return method != null ? lookupMemberRule(method.getDefinition()) : null;
   }
 
   private ProguardMemberRuleLookup lookupMemberRule(DexDefinition definition) {
@@ -245,7 +250,7 @@
       return;
     }
 
-    DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
     ProguardMemberRuleLookup lookup = lookupMemberRule(singleTarget);
     if (lookup == null) {
       // -assumenosideeffects rules are applied to upward visible and overriding methods, but only
@@ -264,7 +269,7 @@
       if (singleTarget != null
           && lookup.type == RuleType.ASSUME_NO_SIDE_EFFECTS
           && !lookup.rule.hasReturnValue()) {
-        ProguardMemberRule rule = appView.appInfo().assumedValues.get(singleTarget.toReference());
+        ProguardMemberRule rule = appView.appInfo().assumedValues.get(singleTarget.getReference());
         if (rule != null) {
           lookup = new ProguardMemberRuleLookup(RuleType.ASSUME_VALUES, rule);
         }
@@ -280,7 +285,8 @@
       return;
     }
 
-    AbstractValue abstractReturnValue = singleTarget.getOptimizationInfo().getAbstractReturnValue();
+    AbstractValue abstractReturnValue =
+        singleTarget.getDefinition().getOptimizationInfo().getAbstractReturnValue();
 
     if (abstractReturnValue.isSingleValue()) {
       SingleValue singleReturnValue = abstractReturnValue.asSingleValue();
@@ -299,7 +305,7 @@
           replaceInstructionByNullCheckIfPossible(invoke, iterator, context);
         } else if (invoke.isInvokeStatic()) {
           replaceInstructionByInitClassIfPossible(
-              invoke, singleTarget.holder(), code, iterator, context);
+              invoke, singleTarget.getHolderType(), code, iterator, context);
         }
 
         // Insert the definition of the replacement.
@@ -309,7 +315,7 @@
         } else {
           iterator.add(replacement);
         }
-        singleTarget.getMutableOptimizationInfo().markAsPropagated();
+        singleTarget.getDefinition().getMutableOptimizationInfo().markAsPropagated();
       }
     }
   }
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 b6bd724..b2ab54f 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
@@ -8,10 +8,11 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndField;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
@@ -148,19 +149,24 @@
     }
   }
 
-  public boolean isFinal(DexEncodedField field) {
-    if (field.isProgramField(appView)) {
-      return field.isFinal();
+  public boolean isFinal(DexClassAndField field) {
+    if (field.isProgramField()) {
+      // Treat this field as being final if it is declared final or we have determined a constant
+      // value for it.
+      return field.getDefinition().isFinal()
+          || field.getDefinition().getOptimizationInfo().getAbstractValue().isSingleValue();
     }
-    return appView.libraryMethodOptimizer().isFinalLibraryField(field);
+    return appView.libraryMethodOptimizer().isFinalLibraryField(field.getDefinition());
   }
 
-  private DexEncodedField resolveField(DexField field) {
+  private DexClassAndField resolveField(DexField field) {
     if (appView.enableWholeProgramOptimizations()) {
-      return appView.appInfo().withLiveness().resolveField(field).getResolvedField();
+      SuccessfulFieldResolutionResult resolutionResult =
+          appView.appInfo().withLiveness().resolveField(field).asSuccessfulResolution();
+      return resolutionResult != null ? resolutionResult.getResolutionPair() : null;
     }
-    if (field.holder == method.getHolderType()) {
-      return method.getHolder().lookupField(field);
+    if (field.getHolderType() == method.getHolderType()) {
+      return method.getHolder().lookupProgramField(field);
     }
     return null;
   }
@@ -187,9 +193,9 @@
         while (it.hasNext()) {
           Instruction instruction = it.next();
           if (instruction.isFieldInstruction()) {
-            DexField field = instruction.asFieldInstruction().getField();
-            DexEncodedField definition = resolveField(field);
-            if (definition == null || definition.isVolatile()) {
+            DexField reference = instruction.asFieldInstruction().getField();
+            DexClassAndField field = resolveField(reference);
+            if (field == null || field.getDefinition().isVolatile()) {
               killAllNonFinalActiveFields();
               continue;
             }
@@ -200,7 +206,7 @@
                 continue;
               }
               Value object = instanceGet.object().getAliasedValue();
-              FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+              FieldAndObject fieldAndObject = new FieldAndObject(reference, object);
               FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
               if (replacement != null) {
                 replacement.eliminateRedundantRead(it, instanceGet);
@@ -215,10 +221,11 @@
               killNonFinalActiveFields(instancePut);
               // ... but at least we know the field value for this particular object.
               Value object = instancePut.object().getAliasedValue();
-              FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+              FieldAndObject fieldAndObject = new FieldAndObject(reference, object);
               ExistingValue value = new ExistingValue(instancePut.value());
-              if (isFinal(definition)) {
-                assert method.getDefinition().isInstanceInitializer()
+              if (isFinal(field)) {
+                assert !field.getDefinition().isFinal()
+                    || method.getDefinition().isInstanceInitializer()
                     || verifyWasInstanceInitializer();
                 activeState.putFinalInstanceField(fieldAndObject, value);
               } else {
@@ -229,7 +236,7 @@
               if (staticGet.outValue().hasLocalInfo()) {
                 continue;
               }
-              FieldValue replacement = activeState.getStaticFieldValue(field);
+              FieldValue replacement = activeState.getStaticFieldValue(reference);
               if (replacement != null) {
                 replacement.eliminateRedundantRead(it, staticGet);
               } else {
@@ -237,10 +244,10 @@
                 // field values.
                 killNonFinalActiveFields(staticGet);
                 FieldValue value = new ExistingValue(staticGet.value());
-                if (isFinal(definition)) {
-                  activeState.putFinalStaticField(field, value);
+                if (isFinal(field)) {
+                  activeState.putFinalStaticField(reference, value);
                 } else {
-                  activeState.putNonFinalStaticField(field, value);
+                  activeState.putNonFinalStaticField(reference, value);
                 }
               }
             } else if (instruction.isStaticPut()) {
@@ -249,11 +256,12 @@
               // field values.
               killNonFinalActiveFields(staticPut);
               ExistingValue value = new ExistingValue(staticPut.value());
-              if (definition.isFinal()) {
-                assert method.getDefinition().isClassInitializer();
-                activeState.putFinalStaticField(field, value);
+              if (isFinal(field)) {
+                assert !field.getDefinition().isFinal()
+                    || method.getDefinition().isClassInitializer();
+                activeState.putFinalStaticField(reference, value);
               } else {
-                activeState.putNonFinalStaticField(field, value);
+                activeState.putNonFinalStaticField(reference, value);
               }
             }
           } else if (instruction.isInitClass()) {
@@ -352,14 +360,14 @@
       return;
     }
 
-    DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, method);
-    if (singleTarget == null || !singleTarget.isInstanceInitializer()) {
+    DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
+    if (singleTarget == null || !singleTarget.getDefinition().isInstanceInitializer()) {
       killAllNonFinalActiveFields();
       return;
     }
 
     InstanceInitializerInfo instanceInitializerInfo =
-        singleTarget.getOptimizationInfo().getInstanceInitializerInfo();
+        singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo();
     if (instanceInitializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
       killAllNonFinalActiveFields();
     }
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 c8cc00e..6b0b61d 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
@@ -6,18 +6,19 @@
 
 import static com.android.tools.r8.graph.DexEncodedMethod.asProgramMethodOrNull;
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 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.LibraryMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
@@ -296,21 +297,22 @@
           }
 
           // TODO(b/156853206): Avoid duplicating resolution.
-          DexEncodedMethod singleTargetMethod = invokeMethod.lookupSingleTarget(appView, method);
-          if (singleTargetMethod == null) {
+          DexClassAndMethod singleTarget = invokeMethod.lookupSingleTarget(appView, method);
+          if (singleTarget == null) {
             return user; // Not eligible.
           }
 
-          if (isEligibleLibraryMethodCall(invokeMethod, singleTargetMethod)) {
+          if (singleTarget.isLibraryMethod()
+              && isEligibleLibraryMethodCall(invokeMethod, singleTarget.asLibraryMethod())) {
             continue;
           }
 
-          ProgramMethod singleTarget = singleTargetMethod.asProgramMethod(appView);
-          if (!isEligibleSingleTarget(singleTarget)) {
+          ProgramMethod singleProgramTarget = singleTarget.asProgramMethod();
+          if (!isEligibleSingleTarget(singleProgramTarget)) {
             return user; // Not eligible.
           }
 
-          if (AccessControl.isClassAccessible(singleTarget.getHolder(), method, appView)
+          if (AccessControl.isClassAccessible(singleProgramTarget.getHolder(), method, appView)
               .isPossiblyFalse()) {
             return user; // Not eligible.
           }
@@ -324,7 +326,7 @@
                       && !invoke.inValues().isEmpty()
                       && root.outValue() == invoke.getReceiver();
               if (isCorrespondingConstructorCall) {
-                InliningInfo inliningInfo = isEligibleConstructorCall(invoke, singleTarget);
+                InliningInfo inliningInfo = isEligibleConstructorCall(invoke, singleProgramTarget);
                 if (inliningInfo != null) {
                   methodCallsOnInstance.put(invoke, inliningInfo);
                   continue;
@@ -340,7 +342,7 @@
             InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
             InliningInfo inliningInfo =
                 isEligibleDirectVirtualMethodCall(
-                    invoke, resolutionResult, singleTarget, indirectUsers, defaultOracle);
+                    invoke, resolutionResult, singleProgramTarget, indirectUsers, defaultOracle);
             if (inliningInfo != null) {
               methodCallsOnInstance.put(invoke, inliningInfo);
               continue;
@@ -351,7 +353,7 @@
           if (isExtraMethodCall(invokeMethod)) {
             assert !invokeMethod.isInvokeSuper();
             assert !invokeMethod.isInvokePolymorphic();
-            if (isExtraMethodCallEligible(invokeMethod, singleTarget, defaultOracle)) {
+            if (isExtraMethodCallEligible(invokeMethod, singleProgramTarget, defaultOracle)) {
               continue;
             }
           }
@@ -646,12 +648,13 @@
           continue;
         }
 
-        DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, method);
-        if (singleTarget != null) {
-          Predicate<InvokeMethod> noSideEffectsPredicate =
-              dexItemFactory.libraryMethodsWithoutSideEffects.getOrDefault(
-                  singleTarget.method, alwaysFalse());
-          if (noSideEffectsPredicate.test(invoke)) {
+        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
+        if (singleTarget != null && singleTarget.isLibraryMethod()) {
+          boolean isSideEffectFree =
+              appView
+                  .getLibraryMethodSideEffectModelCollection()
+                  .isSideEffectFree(invoke, singleTarget.asLibraryMethod());
+          if (isSideEffectFree) {
             if (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers()) {
               removeInstruction(invoke);
               continue;
@@ -1169,13 +1172,13 @@
     return true;
   }
 
-  private boolean isEligibleLibraryMethodCall(InvokeMethod invoke, DexEncodedMethod singleTarget) {
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        dexItemFactory.libraryMethodsWithoutSideEffects.get(singleTarget.method);
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(invoke)) {
+  private boolean isEligibleLibraryMethodCall(InvokeMethod invoke, LibraryMethod singleTarget) {
+    boolean isSideEffectFree =
+        appView.getLibraryMethodSideEffectModelCollection().isSideEffectFree(invoke, singleTarget);
+    if (isSideEffectFree) {
       return !invoke.hasOutValue() || !invoke.outValue().hasAnyUsers();
     }
-    if (singleTarget.method == dexItemFactory.objectsMethods.requireNonNull) {
+    if (singleTarget.getReference() == dexItemFactory.objectsMethods.requireNonNull) {
       return !invoke.hasOutValue() || !invoke.outValue().hasAnyUsers();
     }
     return false;
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 c766569..334e4fa 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
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -211,7 +212,7 @@
     DexMethod invokedMethod = invokeStatic.getInvokedMethod();
     DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder);
     if (enumClass != null) {
-      DexEncodedMethod method = invokeStatic.lookupSingleTarget(appView, context);
+      DexClassAndMethod method = invokeStatic.lookupSingleTarget(appView, context);
       if (method != null) {
         eligibleEnums.add(enumClass.type);
       } else {
@@ -726,19 +727,13 @@
         }
         return Reason.INVALID_INVOKE_ON_ARRAY;
       }
-      DexEncodedMethod encodedSingleTarget =
-          invokeMethod.lookupSingleTarget(appView, code.context());
-      if (encodedSingleTarget == null) {
+      DexClassAndMethod singleTarget = invokeMethod.lookupSingleTarget(appView, code.context());
+      if (singleTarget == null) {
         return Reason.INVALID_INVOKE;
       }
-      DexMethod singleTarget = encodedSingleTarget.method;
-      DexClass dexClass = appView.definitionFor(singleTarget.holder, code.context());
-      if (dexClass == null) {
-        assert false;
-        return Reason.INVALID_INVOKE;
-      }
+      DexClass dexClass = singleTarget.getHolder();
       if (dexClass.isProgramClass()) {
-        if (dexClass.isEnum() && encodedSingleTarget.isInstanceInitializer()) {
+        if (dexClass.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
           if (code.method().holder() == dexClass.type && code.method().isClassInitializer()) {
             // The enum instance initializer is allowed to be called only from the enum clinit.
             return Reason.ELIGIBLE;
@@ -748,10 +743,10 @@
         }
         // Check that the enum-value only flows into parameters whose type exactly matches the
         // enum's type.
-        int offset = BooleanUtils.intValue(!encodedSingleTarget.isStatic());
-        for (int i = 0; i < singleTarget.proto.parameters.size(); i++) {
+        int offset = BooleanUtils.intValue(!singleTarget.getDefinition().isStatic());
+        for (int i = 0; i < singleTarget.getReference().getParameters().size(); i++) {
           if (invokeMethod.getArgument(offset + i) == enumValue) {
-            if (singleTarget.proto.parameters.values[i].toBaseType(factory) != enumClass.type) {
+            if (singleTarget.getReference().getParameter(i).toBaseType(factory) != enumClass.type) {
               return Reason.GENERIC_INVOKE;
             }
           }
@@ -768,43 +763,44 @@
         return Reason.INVALID_INVOKE;
       }
       assert dexClass.isLibraryClass();
+      DexMethod singleTargetReference = singleTarget.getReference();
       if (dexClass.type != factory.enumType) {
         // System.identityHashCode(Object) is supported for proto enums.
         // Object#getClass without outValue and Objects.requireNonNull are supported since R8
         // rewrites explicit null checks to such instructions.
-        if (singleTarget == factory.javaLangSystemMethods.identityHashCode) {
+        if (singleTargetReference == factory.javaLangSystemMethods.identityHashCode) {
           return Reason.ELIGIBLE;
         }
-        if (singleTarget == factory.stringMembers.valueOf) {
+        if (singleTargetReference == factory.stringMembers.valueOf) {
           addRequiredNameData(enumClass.type);
           return Reason.ELIGIBLE;
         }
-        if (singleTarget == factory.objectMembers.getClass
+        if (singleTargetReference == factory.objectMembers.getClass
             && (!invokeMethod.hasOutValue() || !invokeMethod.outValue().hasAnyUsers())) {
           // This is a hidden null check.
           return Reason.ELIGIBLE;
         }
-        if (singleTarget == factory.objectsMethods.requireNonNull
-            || singleTarget == factory.objectsMethods.requireNonNullWithMessage) {
+        if (singleTargetReference == factory.objectsMethods.requireNonNull
+            || singleTargetReference == factory.objectsMethods.requireNonNullWithMessage) {
           return Reason.ELIGIBLE;
         }
         return Reason.UNSUPPORTED_LIBRARY_CALL;
       }
       // TODO(b/147860220): EnumSet and EnumMap may be interesting to model.
-      if (singleTarget == factory.enumMembers.compareTo) {
+      if (singleTargetReference == factory.enumMembers.compareTo) {
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.equals) {
+      } else if (singleTargetReference == factory.enumMembers.equals) {
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.nameMethod
-          || singleTarget == factory.enumMembers.toString) {
+      } else if (singleTargetReference == factory.enumMembers.nameMethod
+          || singleTargetReference == factory.enumMembers.toString) {
         assert invokeMethod.asInvokeMethodWithReceiver().getReceiver() == enumValue;
         addRequiredNameData(enumClass.type);
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.ordinalMethod) {
+      } else if (singleTargetReference == factory.enumMembers.ordinalMethod) {
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.hashCode) {
+      } else if (singleTargetReference == factory.enumMembers.hashCode) {
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.constructor) {
+      } 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) {
           return Reason.ELIGIBLE;
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 9125b1c..cb6a6fa 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
@@ -45,6 +45,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -260,11 +261,11 @@
         case INVOKE_STATIC:
           {
             InvokeStatic invoke = insn.asInvokeStatic();
-            DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+            DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
             if (singleTarget == null) {
               return; // Not allowed.
             }
-            if (singleTarget.method == dexItemFactory.objectsMethods.requireNonNull) {
+            if (singleTarget.getReference() == dexItemFactory.objectsMethods.requireNonNull) {
               if (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers()) {
                 continue;
               }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
index 92635f2..1da512d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.ConstClass;
@@ -65,6 +66,8 @@
 
     boolean isValidInitClass(CodeProcessor context, DexType clazz);
 
+    boolean isValidHolder(CodeProcessor context, DexType holder);
+
     void patch(ApplyStrategy context, NewInstance newInstance);
 
     void patch(ApplyStrategy context, InvokeMethod invoke);
@@ -74,6 +77,8 @@
     void patch(ApplyStrategy context, StaticGet staticGet);
 
     void patch(ApplyStrategy context, InitClass initClass);
+
+    void patch(ApplyStrategy context, Argument argument);
   }
 
   // No-op strategy.
@@ -120,6 +125,11 @@
         }
 
         @Override
+        public boolean isValidHolder(CodeProcessor context, DexType holder) {
+          return false;
+        }
+
+        @Override
         public void patch(ApplyStrategy context, NewInstance newInstance) {
           throw new Unreachable();
         }
@@ -143,6 +153,11 @@
         public void patch(ApplyStrategy context, InitClass initClass) {
           throw new Unreachable();
         }
+
+        @Override
+        public void patch(ApplyStrategy context, Argument argument) {
+          throw new Unreachable();
+        }
       };
 
   public final AppView<AppInfoWithLiveness> appView;
@@ -383,6 +398,20 @@
     return null;
   }
 
+  @Override
+  public Void visit(Argument instruction) {
+    if (instruction.outValue() != code.getThis()) {
+      return null;
+    }
+    Strategy strategy = strategyProvider.apply(method.getHolderType());
+    if (strategy.isValidHolder(this, method.getHolderType())) {
+      if (shouldRewrite(method.getHolderType())) {
+        process(strategy, instruction);
+      }
+    }
+    return null;
+  }
+
   abstract void process(Strategy strategy, InvokeMethod invokeMethod);
 
   abstract void process(Strategy strategy, NewInstance newInstance);
@@ -396,4 +425,6 @@
   abstract void process(Strategy strategy, StaticGet staticGet);
 
   abstract void process(Strategy strategy, InitClass initClass);
+
+  abstract void process(Strategy strategy, Argument argument);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
index 9ccbbbc..8aacbde 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
 import com.android.tools.r8.ir.analysis.type.DestructivePhiTypeUpdater;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InitClass;
 import com.android.tools.r8.ir.code.InstanceGet;
@@ -542,6 +543,11 @@
     void process(Strategy strategy, InitClass initClass) {
       queueForProcessing(method);
     }
+
+    @Override
+    void process(Strategy strategy, Argument argument) {
+      throw new Unreachable();
+    }
   }
 
   public final class ApplyStrategy extends CodeProcessor {
@@ -661,6 +667,11 @@
     void process(Strategy strategy, InitClass initClass) {
       strategy.patch(this, initClass);
     }
+
+    @Override
+    void process(Strategy strategy, Argument argument) {
+      strategy.patch(this, argument);
+    }
   }
 
   private final class LambdaMergerOptimizationInfoFixer
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupCodeStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupCodeStrategy.java
index 112b37c..2c88700 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupCodeStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupCodeStrategy.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.InitClass;
@@ -120,6 +121,12 @@
   }
 
   @Override
+  public boolean isValidHolder(CodeProcessor context, DexType holder) {
+    assert group.containsLambda(holder);
+    return true;
+  }
+
+  @Override
   public void patch(ApplyStrategy context, NewInstance newInstance) {
     DexType oldType = newInstance.clazz;
     DexType newType = group.getGroupClassType();
@@ -217,6 +224,17 @@
     context.instructions().replaceCurrentInstruction(pachedInitClass);
   }
 
+  @Override
+  public void patch(ApplyStrategy context, Argument argument) {
+    // An argument can be a direct operand to a phi that we potentially could not remove.
+    assert argument.getIndex() == 0;
+    // The argument value will be replaced by the invoke value.
+    argument
+        .outValue()
+        .setType(TypeElement.fromDexType(group.getGroupClassType(), maybeNull(), context.appView));
+    context.recordTypeHasChanged(argument.outValue());
+  }
+
   private void patchInitializer(CodeProcessor context, InvokeDirect invoke) {
     // Patching includes:
     //  - change of methods
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
index dcb128a..69bdef8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -39,13 +39,13 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (singleTarget.method == dexItemFactory.booleanMembers.booleanValue) {
+    if (singleTarget.getReference() == dexItemFactory.booleanMembers.booleanValue) {
       optimizeBooleanValue(code, instructionIterator, invoke);
-    } else if (singleTarget.method == dexItemFactory.booleanMembers.parseBoolean) {
+    } else if (singleTarget.getReference() == dexItemFactory.booleanMembers.parseBoolean) {
       optimizeParseBoolean(code, instructionIterator, invoke);
-    } else if (singleTarget.method == dexItemFactory.booleanMembers.valueOf) {
+    } else if (singleTarget.getReference() == dexItemFactory.booleanMembers.valueOf) {
       optimizeValueOf(code, instructionIterator, invoke, affectedValues);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
index 658fcb5..4323220 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
@@ -7,7 +7,7 @@
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -36,9 +36,9 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (singleTarget.method == appView.dexItemFactory().enumMembers.valueOf
+    if (singleTarget.getReference() == appView.dexItemFactory().enumMembers.valueOf
         && invoke.inValues().get(0).isConstClass()) {
       insertAssumeDynamicType(code, instructionIterator, invoke);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index bcea380..d6c8ada 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -5,8 +5,8 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory.LibraryMembers;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -118,7 +118,7 @@
       Instruction instruction = instructionIterator.next();
       if (instruction.isInvokeMethod()) {
         InvokeMethod invoke = instruction.asInvokeMethod();
-        DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
         if (singleTarget != null) {
           optimizeInvoke(code, instructionIterator, invoke, singleTarget, affectedValues);
         }
@@ -133,11 +133,11 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
     LibraryMethodModelCollection optimizer =
         libraryMethodModelCollections.getOrDefault(
-            singleTarget.holder(), NopLibraryMethodModelCollection.getInstance());
+            singleTarget.getHolderType(), NopLibraryMethodModelCollection.getInstance());
     optimizer.optimize(code, instructionIterator, invoke, singleTarget, affectedValues);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
index 5608b94..843e9ab 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.library;
 
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -29,6 +29,6 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
new file mode 100644
index 0000000..55d1dc3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -0,0 +1,82 @@
+// 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.library;
+
+import static com.google.common.base.Predicates.alwaysFalse;
+import static com.google.common.base.Predicates.alwaysTrue;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.LibraryMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+public class LibraryMethodSideEffectModelCollection {
+
+  private final Map<DexMethod, Predicate<InvokeMethod>> finalMethodsWithoutSideEffects;
+  private final Set<DexMethod> nonFinalMethodsWithoutSideEffects;
+
+  public LibraryMethodSideEffectModelCollection(DexItemFactory dexItemFactory) {
+    finalMethodsWithoutSideEffects = buildFinalMethodsWithoutSideEffects(dexItemFactory);
+    nonFinalMethodsWithoutSideEffects = buildNonFinalMethodsWithoutSideEffects(dexItemFactory);
+  }
+
+  private static Map<DexMethod, Predicate<InvokeMethod>> buildFinalMethodsWithoutSideEffects(
+      DexItemFactory dexItemFactory) {
+    ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder =
+        ImmutableMap.<DexMethod, Predicate<InvokeMethod>>builder()
+            .put(dexItemFactory.enumMembers.constructor, alwaysTrue())
+            .put(dexItemFactory.npeMethods.init, alwaysTrue())
+            .put(dexItemFactory.npeMethods.initWithMessage, alwaysTrue())
+            .put(dexItemFactory.objectMembers.constructor, alwaysTrue())
+            .put(dexItemFactory.objectMembers.getClass, alwaysTrue())
+            .put(dexItemFactory.stringMembers.hashCode, alwaysTrue());
+    putAll(builder, dexItemFactory.classMethods.getNames, alwaysTrue());
+    putAll(
+        builder,
+        dexItemFactory.stringBufferMethods.constructorMethods,
+        dexItemFactory.stringBufferMethods::constructorInvokeIsSideEffectFree);
+    putAll(
+        builder,
+        dexItemFactory.stringBuilderMethods.constructorMethods,
+        dexItemFactory.stringBuilderMethods::constructorInvokeIsSideEffectFree);
+    putAll(builder, dexItemFactory.boxedValueOfMethods(), alwaysTrue());
+    return builder.build();
+  }
+
+  private static Set<DexMethod> buildNonFinalMethodsWithoutSideEffects(
+      DexItemFactory dexItemFactory) {
+    return ImmutableSet.of(
+        dexItemFactory.objectMembers.equals,
+        dexItemFactory.objectMembers.hashCode,
+        dexItemFactory.objectMembers.toString);
+  }
+
+  private static void putAll(
+      ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder,
+      Iterable<DexMethod> methods,
+      Predicate<InvokeMethod> predicate) {
+    for (DexMethod method : methods) {
+      builder.put(method, predicate);
+    }
+  }
+
+  public boolean isCallToSideEffectFreeFinalMethod(InvokeMethod invoke) {
+    return finalMethodsWithoutSideEffects
+        .getOrDefault(invoke.getInvokedMethod(), alwaysFalse())
+        .test(invoke);
+  }
+
+  // This intentionally takes the invoke instruction since the determination of whether a library
+  // method has side effects may depend on the arguments.
+  public boolean isSideEffectFree(InvokeMethod invoke, LibraryMethod singleTarget) {
+    return isCallToSideEffectFreeFinalMethod(invoke)
+        || nonFinalMethodsWithoutSideEffects.contains(singleTarget.getReference());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
index 35220c7..f8847d3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -104,11 +104,11 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
     int maxRemovedAndroidLogLevel =
         appView.options().getProguardConfiguration().getMaxRemovedAndroidLogLevel();
-    if (singleTarget.method == isLoggableMethod) {
+    if (singleTarget.getReference() == isLoggableMethod) {
       Value logLevelValue = invoke.arguments().get(1).getAliasedValue();
       if (!logLevelValue.isPhi() && !logLevelValue.hasLocalInfo()) {
         Instruction definition = logLevelValue.definition;
@@ -118,27 +118,27 @@
               code, instructionIterator, invoke, maxRemovedAndroidLogLevel >= logLevel ? 0 : 1);
         }
       }
-    } else if (singleTarget.method == vMethod) {
+    } else if (singleTarget.getReference() == vMethod) {
       if (maxRemovedAndroidLogLevel >= VERBOSE) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
-    } else if (singleTarget.method == dMethod) {
+    } else if (singleTarget.getReference() == dMethod) {
       if (maxRemovedAndroidLogLevel >= DEBUG) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
-    } else if (singleTarget.method == iMethod) {
+    } else if (singleTarget.getReference() == iMethod) {
       if (maxRemovedAndroidLogLevel >= INFO) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
-    } else if (singleTarget.method == wMethod) {
+    } else if (singleTarget.getReference() == wMethod) {
       if (maxRemovedAndroidLogLevel >= WARN) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
-    } else if (singleTarget.method == eMethod) {
+    } else if (singleTarget.getReference() == eMethod) {
       if (maxRemovedAndroidLogLevel >= ERROR) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
-    } else if (singleTarget.method == wtfMethod) {
+    } else if (singleTarget.getReference() == wtfMethod) {
       if (maxRemovedAndroidLogLevel >= ASSERT) {
         replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
index 9a0980f..f852393 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -34,6 +34,6 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {}
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
index 1674baf..f16af45 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.IRCode;
@@ -32,9 +32,9 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (singleTarget.method == dexItemFactory.objectMembers.getClass) {
+    if (singleTarget.getReference() == dexItemFactory.objectMembers.getClass) {
       optimizeGetClass(instructionIterator, invoke);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
index c78a123..8fcc1ac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.IRCode;
@@ -32,9 +32,9 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (dexItemFactory.objectsMethods.isRequireNonNullMethod(singleTarget.method)) {
+    if (dexItemFactory.objectsMethods.isRequireNonNullMethod(singleTarget.getReference())) {
       optimizeRequireNonNull(instructionIterator, invoke, affectedValues);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index 64dce88..18ad3e6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -5,7 +5,7 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
@@ -38,9 +38,9 @@
       IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
-      DexEncodedMethod singleTarget,
+      DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (singleTarget.method == dexItemFactory.stringMembers.equals) {
+    if (singleTarget.getReference() == dexItemFactory.stringMembers.equals) {
       optimizeEquals(code, instructionIterator, invoke);
     }
   }
@@ -74,9 +74,10 @@
       return false;
     }
 
-    DexEncodedMethod singleTarget =
+    DexClassAndMethod singleTarget =
         classNameDefinition.asInvokeVirtual().lookupSingleTarget(appView, context);
-    if (singleTarget == null || singleTarget.method != dexItemFactory.classMethods.getName) {
+    if (singleTarget == null
+        || singleTarget.getReference() != dexItemFactory.classMethods.getName) {
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
index 7e66078..9b12469 100644
--- a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
@@ -60,6 +60,11 @@
   }
 
   @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    return getPrevious().getOriginalTypes(type);
+  }
+
+  @Override
   public DexField getOriginalFieldSignature(DexField field) {
     return getPrevious().getOriginalFieldSignature(field);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 3f625f2..02b56e3 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -84,6 +84,11 @@
   }
 
   @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    return getPrevious().getOriginalTypes(type);
+  }
+
+  @Override
   public DexField getOriginalFieldSignature(DexField field) {
     return getPrevious().getOriginalFieldSignature(field);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 9e23dd0..867ec0b 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -52,6 +52,11 @@
   }
 
   @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    return getPrevious().getOriginalTypes(type);
+  }
+
+  @Override
   public DexField getOriginalFieldSignature(DexField field) {
     return getPrevious().getOriginalFieldSignature(field);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
index 9fe3e29..6a6c3bb 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingLens.java
@@ -49,6 +49,11 @@
   }
 
   @Override
+  public Iterable<DexType> getOriginalTypes(DexType type) {
+    return getPrevious().getOriginalTypes(type);
+  }
+
+  @Override
   public DexField getOriginalFieldSignature(DexField field) {
     return getPrevious().getOriginalFieldSignature(field);
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index 9b838df..bd537a6 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -18,7 +18,10 @@
 import com.android.tools.r8.retrace.internal.RetraceRegularExpression;
 import com.android.tools.r8.retrace.internal.RetracerImpl;
 import com.android.tools.r8.retrace.internal.StackTraceElementProxyRetracerImpl;
+import com.android.tools.r8.retrace.internal.StackTraceElementProxyRetracerImpl.RetraceStackTraceProxyImpl;
 import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy;
+import com.android.tools.r8.retrace.internal.StackTraceVisitor;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.OptionsParsing;
 import com.android.tools.r8.utils.OptionsParsing.ParseContext;
@@ -35,7 +38,9 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Scanner;
 
 /**
@@ -169,38 +174,53 @@
       timing.end();
       RetraceCommandLineResult result;
       timing.begin("Parse and Retrace");
-      if (command.regularExpression != null) {
-        result =
-            new RetraceRegularExpression(
-                    retracer,
-                    command.stackTrace,
-                    command.diagnosticsHandler,
-                    command.regularExpression,
-                    command.isVerbose)
-                .retrace();
-      } else {
-        PlainStackTraceVisitor plainStackTraceVisitor =
-            new PlainStackTraceVisitor(command.stackTrace, command.diagnosticsHandler);
+      StackTraceVisitor<StackTraceElementStringProxy> stackTraceVisitor =
+          command.regularExpression != null
+              ? new RetraceRegularExpression(
+                  retracer, command.stackTrace, command.regularExpression)
+              : new PlainStackTraceVisitor(command.stackTrace, command.diagnosticsHandler);
         StackTraceElementProxyRetracer<StackTraceElementStringProxy> proxyRetracer =
             new StackTraceElementProxyRetracerImpl<>(retracer);
         List<String> retracedStrings = new ArrayList<>();
-        plainStackTraceVisitor.forEach(
-            stackTraceElement -> {
-              List<String> retracedStringsForElement = new ArrayList<>();
-              proxyRetracer
-                  .retrace(stackTraceElement)
-                  .forEach(
-                      retracedElement -> {
-                        StackTraceElementStringProxy originalItem =
-                            retracedElement.getOriginalItem();
-                        retracedStringsForElement.add(
-                            originalItem.toRetracedItem(
-                                retracedElement, !retracedStringsForElement.isEmpty()));
-                      });
-              retracedStrings.addAll(retracedStringsForElement);
-            });
+      stackTraceVisitor.forEach(
+          stackTraceElement -> {
+            Box<List<RetraceStackTraceProxyImpl<StackTraceElementStringProxy>>> currentList =
+                new Box<>();
+            Map<
+                    RetraceStackTraceProxyImpl<StackTraceElementStringProxy>,
+                    List<RetraceStackTraceProxyImpl<StackTraceElementStringProxy>>>
+                ambiguousBlocks = new HashMap<>();
+            proxyRetracer
+                .retrace(stackTraceElement)
+                .forEach(
+                    retracedElement -> {
+                      if (retracedElement.isTopFrame() || !retracedElement.hasRetracedClass()) {
+                        List<RetraceStackTraceProxyImpl<StackTraceElementStringProxy>> block =
+                            new ArrayList<>();
+                        ambiguousBlocks.put(retracedElement, block);
+                        currentList.set(block);
+                      }
+                      currentList.get().add(retracedElement);
+                    });
+            ambiguousBlocks.keySet().stream()
+                .sorted()
+                .forEach(
+                    topFrame -> {
+                      ambiguousBlocks
+                          .get(topFrame)
+                          .forEach(
+                              frame -> {
+                                StackTraceElementStringProxy originalItem = frame.getOriginalItem();
+                                retracedStrings.add(
+                                    originalItem.toRetracedItem(
+                                        frame, !currentList.isSet(), command.isVerbose));
+                                // Use the current list as indicator for us seeing the first
+                                // sorted element.
+                                currentList.set(null);
+                              });
+                    });
+          });
         result = new RetraceCommandLineResult(retracedStrings);
-      }
       timing.end();
       timing.begin("Report result");
       command.retracedStackTraceConsumer.accept(result.getNodes());
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
index f141080..d3b6014 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceFieldResult.java
@@ -27,5 +27,7 @@
     RetraceFieldResult getRetraceFieldResult();
 
     RetraceClassResult.Element getClassElement();
+
+    RetraceSourceFileResult retraceSourceFile(String sourceFile);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java
index b69c237..d9d1375 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceStackTraceProxy.java
@@ -5,26 +5,42 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
+import java.util.List;
 
 @Keep
-public interface RetraceStackTraceProxy<T extends StackTraceElementProxy<?>> {
+public interface RetraceStackTraceProxy<T extends StackTraceElementProxy<?>>
+    extends Comparable<RetraceStackTraceProxy<T>> {
 
   boolean isAmbiguous();
 
+  boolean isTopFrame();
+
   boolean hasRetracedClass();
 
   boolean hasRetracedMethod();
 
+  boolean hasRetracedField();
+
   boolean hasSourceFile();
 
   boolean hasLineNumber();
 
+  boolean hasFieldOrReturnType();
+
+  boolean hasMethodArguments();
+
   T getOriginalItem();
 
   RetracedClass getRetracedClass();
 
   RetracedMethod getRetracedMethod();
 
+  RetracedField getRetracedField();
+
+  RetracedType getRetracedFieldOrReturnType();
+
+  List<RetracedType> getMethodArguments();
+
   String getSourceFile();
 
   int getLineNumber();
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceTypeResult.java b/src/main/java/com/android/tools/r8/retrace/RetraceTypeResult.java
index b9fa2d3..2c0c1fa 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceTypeResult.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceTypeResult.java
@@ -5,7 +5,6 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
-import com.android.tools.r8.retrace.internal.RetracedTypeImpl;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
@@ -21,6 +20,6 @@
   @Keep
   interface Element {
 
-    RetracedTypeImpl getType();
+    RetracedType getType();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetracedClassMember.java b/src/main/java/com/android/tools/r8/retrace/RetracedClassMember.java
index 10aa77c..f28b87e 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetracedClassMember.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetracedClassMember.java
@@ -5,10 +5,9 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
-import com.android.tools.r8.retrace.internal.RetracedClassImpl;
 
 @Keep
 public interface RetracedClassMember {
 
-  RetracedClassImpl getHolderClass();
+  RetracedClass getHolderClass();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/RetracedMethod.java b/src/main/java/com/android/tools/r8/retrace/RetracedMethod.java
index bfc95e9..936ea3e 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetracedMethod.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetracedMethod.java
@@ -10,7 +10,7 @@
 import java.util.List;
 
 @Keep
-public interface RetracedMethod extends RetracedClassMember {
+public interface RetracedMethod extends RetracedClassMember, Comparable<RetracedMethod> {
 
   boolean isUnknown();
 
diff --git a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
index efaab05..09c1a8f 100644
--- a/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/StackTraceElementProxy.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.retrace;
 
 import com.android.tools.r8.Keep;
+import com.android.tools.r8.references.ClassReference;
 
 @Keep
 public abstract class StackTraceElementProxy<T> {
@@ -17,11 +18,23 @@
 
   public abstract boolean hasLineNumber();
 
-  public abstract String className();
+  public abstract boolean hasFieldName();
 
-  public abstract String methodName();
+  public abstract boolean hasFieldOrReturnType();
 
-  public abstract String fileName();
+  public abstract boolean hasMethodArguments();
 
-  public abstract int lineNumber();
+  public abstract ClassReference getClassReference();
+
+  public abstract String getMethodName();
+
+  public abstract String getFileName();
+
+  public abstract int getLineNumber();
+
+  public abstract String getFieldName();
+
+  public abstract String getFieldOrReturnType();
+
+  public abstract String getMethodArguments();
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/FieldDefinition.java b/src/main/java/com/android/tools/r8/retrace/internal/FieldDefinition.java
index 4a3b93c..27d9a54 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/FieldDefinition.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/FieldDefinition.java
@@ -43,7 +43,7 @@
 
     @Override
     FieldDefinition substituteHolder(ClassReference newHolder) {
-      return FieldDefinition.create(classReference, name);
+      return FieldDefinition.create(newHolder, name);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/PlainStackTraceVisitor.java b/src/main/java/com/android/tools/r8/retrace/internal/PlainStackTraceVisitor.java
index 0aac05f..7f7d7fc 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/PlainStackTraceVisitor.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/PlainStackTraceVisitor.java
@@ -7,6 +7,7 @@
 import static com.google.common.base.Predicates.not;
 
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.ClassNameType;
 import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.StackTraceElementStringProxyBuilder;
 import com.android.tools.r8.utils.DescriptorUtils;
 import java.util.List;
@@ -92,7 +93,7 @@
         return null;
       }
       return StackTraceElementStringProxy.builder(line)
-          .registerClassName(exceptionStartIndex, messageStartIndex)
+          .registerClassName(exceptionStartIndex, messageStartIndex, ClassNameType.TYPENAME)
           .build();
     }
   }
@@ -161,7 +162,7 @@
       }
       StackTraceElementStringProxyBuilder builder =
           StackTraceElementStringProxy.builder(line)
-              .registerClassName(classStartIndex, methodSeparator)
+              .registerClassName(classStartIndex, methodSeparator, ClassNameType.TYPENAME)
               .registerMethodName(methodSeparator + 1, parensStart);
       // Check if we have a filename and position.
       int separatorIndex = firstCharFromIndex(line, parensStart, ':');
@@ -201,7 +202,7 @@
         return null;
       }
       return StackTraceElementStringProxy.builder(line)
-          .registerClassName(exceptionStartIndex, lastBracketPosition)
+          .registerClassName(exceptionStartIndex, lastBracketPosition, ClassNameType.TYPENAME)
           .build();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
index 066b979..3a65b0a 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceFieldResultImpl.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.retrace.RetraceFieldResult;
-import com.android.tools.r8.retrace.Retracer;
+import com.android.tools.r8.retrace.RetraceSourceFileResult;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.Pair;
 import java.util.List;
@@ -22,13 +22,13 @@
   private final RetraceClassResultImpl classResult;
   private final List<Pair<RetraceClassResultImpl.ElementImpl, List<MemberNaming>>> memberNamings;
   private final FieldDefinition fieldDefinition;
-  private final Retracer retracer;
+  private final RetracerImpl retracer;
 
   RetraceFieldResultImpl(
       RetraceClassResultImpl classResult,
       List<Pair<RetraceClassResultImpl.ElementImpl, List<MemberNaming>>> memberNamings,
       FieldDefinition fieldDefinition,
-      Retracer retracer) {
+      RetracerImpl retracer) {
     this.classResult = classResult;
     this.memberNamings = memberNamings;
     this.fieldDefinition = fieldDefinition;
@@ -131,5 +131,11 @@
     public RetraceClassResultImpl.ElementImpl getClassElement() {
       return classElement;
     }
+
+    @Override
+    public RetraceSourceFileResult retraceSourceFile(String sourceFile) {
+      return RetraceUtils.getSourceFile(
+          classElement, fieldReference.getHolderClass(), sourceFile, retraceFieldResult.retracer);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetraceRegularExpression.java b/src/main/java/com/android/tools/r8/retrace/internal/RetraceRegularExpression.java
index 6ba4d1a..ca9a2f8 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetraceRegularExpression.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetraceRegularExpression.java
@@ -4,42 +4,19 @@
 
 package com.android.tools.r8.retrace.internal;
 
-import static com.android.tools.r8.retrace.internal.RetraceUtils.methodDescriptionFromRetraceMethod;
-
-import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.references.TypeReference;
-import com.android.tools.r8.retrace.RetraceClassResult;
-import com.android.tools.r8.retrace.RetraceFieldResult;
-import com.android.tools.r8.retrace.RetraceFrameResult;
-import com.android.tools.r8.retrace.RetraceFrameResult.Element;
-import com.android.tools.r8.retrace.RetraceSourceFileResult;
-import com.android.tools.r8.retrace.RetracedClass;
-import com.android.tools.r8.retrace.RetracedField;
-import com.android.tools.r8.retrace.RetracedField.KnownRetracedField;
-import com.android.tools.r8.retrace.RetracedMethod;
-import com.android.tools.r8.retrace.internal.RetraceRegularExpression.ClassNameGroup.ClassNameGroupHandler;
-import com.android.tools.r8.utils.Box;
-import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.StringUtils;
-import com.google.common.collect.Lists;
+import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.ClassNameType;
+import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.StackTraceElementStringProxyBuilder;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
-import java.util.function.BiConsumer;
-import java.util.function.Function;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
 
-public class RetraceRegularExpression {
+public class RetraceRegularExpression implements StackTraceVisitor<StackTraceElementStringProxy> {
 
   private final RetracerImpl retracer;
   private final List<String> stackTrace;
-  private final DiagnosticsHandler diagnosticsHandler;
   private final String regularExpression;
 
   private static final int NO_MATCH = -1;
@@ -52,28 +29,21 @@
   private final LineNumberGroup lineNumberGroup = new LineNumberGroup();
   private final FieldOrReturnTypeGroup fieldOrReturnTypeGroup = new FieldOrReturnTypeGroup();
   private final MethodArgumentsGroup methodArgumentsGroup = new MethodArgumentsGroup();
-
-  private final MethodNameGroup methodNameGroup;
-  private final FieldNameGroup fieldNameGroup;
+  private final MethodNameGroup methodNameGroup = new MethodNameGroup();
+  private final FieldNameGroup fieldNameGroup = new FieldNameGroup();
 
   private static final String CAPTURE_GROUP_PREFIX = "captureGroup";
   private static final int FIRST_CAPTURE_GROUP_INDEX = 0;
 
   public RetraceRegularExpression(
-      RetracerImpl retracer,
-      List<String> stackTrace,
-      DiagnosticsHandler diagnosticsHandler,
-      String regularExpression,
-      boolean isVerbose) {
+      RetracerImpl retracer, List<String> stackTrace, String regularExpression) {
     this.retracer = retracer;
     this.stackTrace = stackTrace;
-    this.diagnosticsHandler = diagnosticsHandler;
     this.regularExpression = regularExpression;
-    methodNameGroup = new MethodNameGroup(isVerbose);
-    fieldNameGroup = new FieldNameGroup(isVerbose);
   }
 
-  public RetraceCommandLineResult retrace() {
+  @Override
+  public void forEach(Consumer<StackTraceElementStringProxy> consumer) {
     List<RegularExpressionGroupHandler> handlers = new ArrayList<>();
     StringBuilder refinedRegularExpressionBuilder = new StringBuilder();
     registerGroups(
@@ -83,74 +53,22 @@
         FIRST_CAPTURE_GROUP_INDEX);
     String refinedRegularExpression = refinedRegularExpressionBuilder.toString();
     Pattern compiledPattern = Pattern.compile(refinedRegularExpression);
-    List<String> result = new ArrayList<>();
     for (String string : stackTrace) {
+      StackTraceElementStringProxyBuilder proxyBuilder =
+          StackTraceElementStringProxy.builder(string);
       Matcher matcher = compiledPattern.matcher(string);
-      if (!matcher.matches()) {
-        result.add(string);
-        continue;
-      }
-      // Iterate through handlers to set contexts. That will allow us to process all handlers from
-      // left to right.
-      RetraceStringContext initialContext = RetraceStringContext.empty();
-      for (RegularExpressionGroupHandler handler : handlers) {
-        initialContext = handler.buildInitial(initialContext, matcher, retracer);
-      }
-      final RetraceString initialRetraceString = RetraceString.start(initialContext);
-      List<RetraceString> retracedStrings = Lists.newArrayList(initialRetraceString);
-      for (RegularExpressionGroupHandler handler : handlers) {
-        retracedStrings = handler.handleMatch(string, retracedStrings, matcher, retracer);
-      }
-      if (retracedStrings.isEmpty()) {
-        // We could not find a match. Output the identity.
-        result.add(string);
-      }
-      boolean isAmbiguous = retracedStrings.size() > 1 && retracedStrings.get(0).isAmbiguous();
-      if (isAmbiguous) {
-        retracedStrings.sort(new RetraceLineComparator());
-      }
-      RetracedClass previousContext = null;
-      for (RetraceString retracedString : retracedStrings) {
-        String finalString = retracedString.builder.build(string);
-        if (!isAmbiguous) {
-          result.add(finalString);
-          continue;
+      if (matcher.matches()) {
+        boolean seenMatchedClassHandler = false;
+        for (RegularExpressionGroupHandler handler : handlers) {
+          if (seenMatchedClassHandler && handler.isClassHandler()) {
+            continue;
+          }
+          if (handler.matchHandler(proxyBuilder, matcher)) {
+            seenMatchedClassHandler |= handler.isClassHandler();
+          }
         }
-        assert retracedString.getClassContext() != null;
-        RetracedClass currentContext = retracedString.getClassContext().getRetracedClass();
-        if (currentContext.equals(previousContext)) {
-          int firstNonWhitespaceCharacter = StringUtils.firstNonWhitespaceCharacter(finalString);
-          finalString =
-              finalString.substring(0, firstNonWhitespaceCharacter)
-                  + "<OR> "
-                  + finalString.substring(firstNonWhitespaceCharacter);
-        }
-        previousContext = currentContext;
-        result.add(finalString);
       }
-    }
-    return new RetraceCommandLineResult(result);
-  }
-
-  static class RetraceLineComparator extends AmbiguousComparator<RetraceString> {
-
-    RetraceLineComparator() {
-      super(
-          (line, t) -> {
-            switch (t) {
-              case CLASS:
-                return line.getClassContext().getRetracedClass().getTypeName();
-              case METHOD:
-                return line.getMethodContext().getTopFrame().getMethodName();
-              case SOURCE:
-                return line.getSource();
-              case LINE:
-                return line.getLineNumber() + "";
-              default:
-                assert false;
-            }
-            throw new RuntimeException("Comparator key is unknown");
-          });
+      consumer.accept(proxyBuilder.build());
     }
   }
 
@@ -160,7 +78,6 @@
       List<RegularExpressionGroupHandler> handlers,
       int captureGroupIndex) {
     int lastCommittedIndex = 0;
-    RegularExpressionGroupHandler lastHandler = null;
     boolean seenPercentage = false;
     boolean escaped = false;
     for (int i = 0; i < regularExpression.length(); i++) {
@@ -180,23 +97,7 @@
               .append(">")
               .append(group.subExpression())
               .append(")");
-          final RegularExpressionGroupHandler handler = group.createHandler(captureGroupName);
-          // If we see a pattern as %c.%m or %C/%m, then register the groups to allow delaying
-          // writing of the class string until we have the fully qualified member.
-          if (lastHandler != null
-              && handler.isQualifiedHandler()
-              && lastHandler.isClassNameGroupHandler()
-              && lastCommittedIndex == i - 3
-              && isTypeOrBinarySeparator(regularExpression, lastCommittedIndex, i - 2)) {
-            final ClassNameGroupHandler classNameGroupHandler =
-                lastHandler.asClassNameGroupHandler();
-            final QualifiedRegularExpressionGroupHandler qualifiedHandler =
-                handler.asQualifiedHandler();
-            classNameGroupHandler.setQualifiedHandler(qualifiedHandler);
-            qualifiedHandler.setClassNameGroupHandler(classNameGroupHandler);
-          }
-          lastHandler = handler;
-          handlers.add(handler);
+          handlers.add(group.createHandler(captureGroupName));
         }
         lastCommittedIndex = i + 1;
         seenPercentage = false;
@@ -247,250 +148,13 @@
     }
   }
 
-  static class RetraceStringContext {
-    private final RetraceClassResult.Element classContext;
-    private final RetraceFrameResult.Element methodContext;
-    private final RetracedClass qualifiedClassContext;
-    private final RetracedMethod qualifiedMethodContext;
-    private final String methodName;
-    private final int minifiedLineNumber;
-    private final int originalLineNumber;
-    private final String source;
-    private final boolean isAmbiguous;
-
-    private RetraceStringContext(
-        RetraceClassResult.Element classContext,
-        RetraceFrameResult.Element methodContext,
-        RetracedClass qualifiedClassContext,
-        RetracedMethod qualifiedMethodContext,
-        String methodName,
-        int minifiedLineNumber,
-        int originalLineNumber,
-        String source,
-        boolean isAmbiguous) {
-      this.classContext = classContext;
-      this.methodContext = methodContext;
-      this.qualifiedClassContext = qualifiedClassContext;
-      this.qualifiedMethodContext = qualifiedMethodContext;
-      this.methodName = methodName;
-      this.minifiedLineNumber = minifiedLineNumber;
-      this.originalLineNumber = originalLineNumber;
-      this.source = source;
-      this.isAmbiguous = isAmbiguous;
-    }
-
-    private static RetraceStringContext empty() {
-      return new RetraceStringContext(
-          null, null, null, null, null, NO_MATCH, NO_MATCH, null, false);
-    }
-
-    private RetraceStringContext withClassContext(
-        RetraceClassResult.Element classContext, RetracedClass qualifiedContext) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    private RetraceStringContext withMethodName(String methodName) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedClassContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    private RetraceStringContext withMethodContext(Element methodContext, boolean isAmbiguous) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedClassContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    private RetraceStringContext withQualifiedClassContext(RetracedClass qualifiedContext) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    private RetraceStringContext withQualifiedMethodContext(RetracedMethod qualifiedContext) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedContext.getHolderClass(),
-          qualifiedContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    public RetraceStringContext withSource(String source) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedClassContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-
-    public RetraceStringContext withLineNumbers(int minifiedLineNumber, int originalLineNumber) {
-      return new RetraceStringContext(
-          classContext,
-          methodContext,
-          qualifiedClassContext,
-          qualifiedMethodContext,
-          methodName,
-          minifiedLineNumber,
-          originalLineNumber,
-          source,
-          isAmbiguous);
-    }
-  }
-
-  static class RetraceStringBuilder {
-
-    private final StringBuilder retracedString;
-    private int lastCommittedIndex;
-
-    private RetraceStringBuilder(String retracedString, int lastCommittedIndex) {
-      this.retracedString = new StringBuilder(retracedString);
-      this.lastCommittedIndex = lastCommittedIndex;
-    }
-
-    private void appendRetracedString(
-        String source, String stringToAppend, int originalFromIndex, int originalToIndex) {
-      retracedString.append(source, lastCommittedIndex, originalFromIndex);
-      retracedString.append(stringToAppend);
-      lastCommittedIndex = originalToIndex;
-    }
-
-    private String build(String source) {
-      return retracedString.append(source, lastCommittedIndex, source.length()).toString();
-    }
-  }
-
-  private static class RetraceString {
-
-    private final RetraceStringBuilder builder;
-    private final RetraceStringContext context;
-
-    private RetraceString(RetraceStringBuilder builder, RetraceStringContext context) {
-      this.builder = builder;
-      this.context = context;
-    }
-
-    private RetraceClassResult.Element getClassContext() {
-      return context.classContext;
-    }
-
-    private RetraceFrameResult.Element getMethodContext() {
-      return context.methodContext;
-    }
-
-    private String getSource() {
-      return context.source;
-    }
-
-    private int getLineNumber() {
-      return context.originalLineNumber;
-    }
-
-    private boolean isAmbiguous() {
-      return context.isAmbiguous;
-    }
-
-    private static RetraceString start(RetraceStringContext initialContext) {
-      return new RetraceString(new RetraceStringBuilder("", 0), initialContext);
-    }
-
-    private RetraceString updateContext(
-        Function<RetraceStringContext, RetraceStringContext> update) {
-      return new RetraceString(builder, update.apply(context));
-    }
-
-    private RetraceString duplicate(RetraceStringContext newContext) {
-      return new RetraceString(
-          new RetraceStringBuilder(builder.retracedString.toString(), builder.lastCommittedIndex),
-          newContext);
-    }
-
-    private RetraceString appendRetracedString(
-        String source, String stringToAppend, int originalFromIndex, int originalToIndex) {
-      builder.appendRetracedString(source, stringToAppend, originalFromIndex, originalToIndex);
-      return this;
-    }
-  }
-
   private interface RegularExpressionGroupHandler {
 
-    List<RetraceString> handleMatch(
-        String original, List<RetraceString> strings, Matcher matcher, RetracerImpl retracer);
+    boolean matchHandler(StackTraceElementStringProxyBuilder builder, Matcher matcher);
 
-    default RetraceStringContext buildInitial(
-        RetraceStringContext context, Matcher matcher, RetracerImpl retracer) {
-      return context;
-    }
-
-    default boolean isClassNameGroupHandler() {
+    default boolean isClassHandler() {
       return false;
     }
-
-    default ClassNameGroupHandler asClassNameGroupHandler() {
-      return null;
-    }
-
-    default boolean isQualifiedHandler() {
-      return false;
-    }
-
-    default QualifiedRegularExpressionGroupHandler asQualifiedHandler() {
-      return null;
-    }
-  }
-
-  private interface QualifiedRegularExpressionGroupHandler extends RegularExpressionGroupHandler {
-
-    @Override
-    default boolean isQualifiedHandler() {
-      return true;
-    }
-
-    @Override
-    default QualifiedRegularExpressionGroupHandler asQualifiedHandler() {
-      return this;
-    }
-
-    void setClassNameGroupHandler(ClassNameGroupHandler handler);
   }
 
   private abstract static class RegularExpressionGroup {
@@ -513,107 +177,31 @@
 
   abstract static class ClassNameGroup extends RegularExpressionGroup {
 
-    abstract String getClassName(RetracedClass classReference);
-
-    abstract ClassReference classFromMatch(String match);
+    abstract ClassNameType getClassNameType();
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return new ClassNameGroupHandler(this, captureGroup);
-    }
-
-    static class ClassNameGroupHandler implements RegularExpressionGroupHandler {
-
-      private RetraceClassResultImpl retraceClassResult = null;
-      private final ClassNameGroup classNameGroup;
-      private final String captureGroup;
-      private RegularExpressionGroupHandler qualifiedHandler;
-
-      public ClassNameGroupHandler(ClassNameGroup classNameGroup, String captureGroup) {
-        this.classNameGroup = classNameGroup;
-        this.captureGroup = captureGroup;
-      }
-
-      @Override
-      public List<RetraceString> handleMatch(
-          String original, List<RetraceString> strings, Matcher matcher, RetracerImpl retracer) {
-        final int startOfGroup = matcher.start(captureGroup);
-        if (startOfGroup == NO_MATCH) {
-          return strings;
+      return new RegularExpressionGroupHandler() {
+        @Override
+        public boolean matchHandler(StackTraceElementStringProxyBuilder builder, Matcher matcher) {
+          int startOfGroup = matcher.start(captureGroup);
+          if (startOfGroup == NO_MATCH) {
+            return false;
+          }
+          String typeName = matcher.group(captureGroup);
+          if (typeName.equals("Suppressed")) {
+            // Ensure we do not map supressed.
+            return false;
+          }
+          builder.registerClassName(startOfGroup, matcher.end(captureGroup), getClassNameType());
+          return true;
         }
-        String typeName = matcher.group(captureGroup);
-        RetraceClassResultImpl retraceResult =
-            retraceClassResult == null
-                ? retracer.retraceClass(classNameGroup.classFromMatch(typeName))
-                : retraceClassResult;
-        assert !retraceResult.isAmbiguous();
-        List<RetraceString> retraceStrings = new ArrayList<>(strings.size());
-        for (RetraceString retraceString : strings) {
-          retraceResult.forEach(
-              element -> {
-                RetraceString newRetraceString =
-                    retraceString.updateContext(
-                        context -> context.withClassContext(element, element.getRetracedClass()));
-                retraceStrings.add(newRetraceString);
-                if (qualifiedHandler == null) {
-                  // If there is no qualified handler, commit right away.
-                  newRetraceString.builder.appendRetracedString(
-                      original,
-                      classNameGroup.getClassName(element.getRetracedClass()),
-                      startOfGroup,
-                      matcher.end(captureGroup));
-                }
-              });
+
+        @Override
+        public boolean isClassHandler() {
+          return true;
         }
-        return retraceStrings;
-      }
-
-      void commitClassName(
-          String original,
-          RetraceString retraceString,
-          RetracedClass qualifiedContext,
-          Matcher matcher) {
-        if (matcher.start(captureGroup) == NO_MATCH) {
-          return;
-        }
-        retraceString.builder.appendRetracedString(
-            original,
-            classNameGroup.getClassName(qualifiedContext),
-            matcher.start(captureGroup),
-            matcher.end(captureGroup));
-      }
-
-      @Override
-      public RetraceStringContext buildInitial(
-          RetraceStringContext context, Matcher matcher, RetracerImpl retracer) {
-        // Reset the local class context since this the same handler is used for multiple lines.
-        retraceClassResult = null;
-        if (matcher.start(captureGroup) == NO_MATCH || context.classContext != null) {
-          return context;
-        }
-        String typeName = matcher.group(captureGroup);
-        retraceClassResult = retracer.retraceClass(classNameGroup.classFromMatch(typeName));
-        assert !retraceClassResult.isAmbiguous();
-        Box<RetraceStringContext> box = new Box<>();
-        retraceClassResult.forEach(
-            element -> box.set(context.withClassContext(element, element.getRetracedClass())));
-        return box.get();
-      }
-
-      public void setQualifiedHandler(RegularExpressionGroupHandler handler) {
-        assert handler.isQualifiedHandler();
-        this.qualifiedHandler = handler;
-      }
-
-      @Override
-      public boolean isClassNameGroupHandler() {
-        return true;
-      }
-
-      @Override
-      public ClassNameGroupHandler asClassNameGroupHandler() {
-        return this;
-      }
+      };
     }
   }
 
@@ -625,13 +213,8 @@
     }
 
     @Override
-    String getClassName(RetracedClass classReference) {
-      return classReference.getTypeName();
-    }
-
-    @Override
-    ClassReference classFromMatch(String match) {
-      return Reference.classFromTypeName(match);
+    ClassNameType getClassNameType() {
+      return ClassNameType.TYPENAME;
     }
   }
 
@@ -643,24 +226,13 @@
     }
 
     @Override
-    String getClassName(RetracedClass classReference) {
-      return classReference.getBinaryName();
-    }
-
-    @Override
-    ClassReference classFromMatch(String match) {
-      return Reference.classFromBinaryName(match);
+    ClassNameType getClassNameType() {
+      return ClassNameType.BINARY;
     }
   }
 
   private static class MethodNameGroup extends RegularExpressionGroup {
 
-    private final boolean printVerbose;
-
-    public MethodNameGroup(boolean printVerbose) {
-      this.printVerbose = printVerbose;
-    }
-
     @Override
     String subExpression() {
       return METHOD_NAME_REGULAR_EXPRESSION;
@@ -668,98 +240,19 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return new QualifiedRegularExpressionGroupHandler() {
-
-        private ClassNameGroupHandler classNameGroupHandler;
-
-        @Override
-        public void setClassNameGroupHandler(ClassNameGroupHandler handler) {
-          classNameGroupHandler = handler;
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
+        if (startOfGroup == NO_MATCH) {
+          return false;
         }
-
-        @Override
-        public List<RetraceString> handleMatch(
-            String original, List<RetraceString> strings, Matcher matcher, RetracerImpl retracer) {
-          final int startOfGroup = matcher.start(captureGroup);
-          if (startOfGroup == NO_MATCH) {
-            if (classNameGroupHandler != null) {
-              for (RetraceString string : strings) {
-                classNameGroupHandler.commitClassName(
-                    original, string, string.context.qualifiedClassContext, matcher);
-              }
-            }
-            return strings;
-          }
-          String methodName = matcher.group(captureGroup);
-          List<RetraceString> retracedStrings = new ArrayList<>();
-          for (RetraceString retraceString : strings) {
-            retraceMethodForString(
-                retraceString,
-                methodName,
-                (element, newContext) -> {
-                  element.visitFrames(
-                      (method, ignoredPosition) -> {
-                        RetraceString newRetraceString =
-                            retraceString.duplicate(newContext.withQualifiedMethodContext(method));
-                        if (classNameGroupHandler != null) {
-                          classNameGroupHandler.commitClassName(
-                              original, newRetraceString, method.getHolderClass(), matcher);
-                        }
-                        retracedStrings.add(
-                            newRetraceString.appendRetracedString(
-                                original,
-                                printVerbose
-                                    ? methodDescriptionFromRetraceMethod(method, false, true)
-                                    : method.getMethodName(),
-                                startOfGroup,
-                                matcher.end(captureGroup)));
-                      });
-                });
-          }
-          return retracedStrings;
-        }
-
-        @Override
-        public RetraceStringContext buildInitial(
-            RetraceStringContext context, Matcher matcher, RetracerImpl retracer) {
-          final int startOfGroup = matcher.start(captureGroup);
-          if (startOfGroup == NO_MATCH || context.methodName != null) {
-            return context;
-          }
-          return context.withMethodName(matcher.group(captureGroup));
-        }
+        builder.registerMethodName(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
-
-    private static void retraceMethodForString(
-        RetraceString retraceString,
-        String methodName,
-        BiConsumer<Element, RetraceStringContext> process) {
-      if (retraceString.context.classContext == null) {
-        return;
-      }
-      RetraceClassResult.Element classContext = retraceString.getClassContext();
-      RetraceFrameResult retraceFrameResult =
-          retraceString.context.minifiedLineNumber > NO_MATCH
-              ? classContext.lookupFrame(methodName, retraceString.context.minifiedLineNumber)
-              : classContext.lookupFrame(methodName);
-      retraceFrameResult.forEach(
-          element -> {
-            process.accept(
-                element,
-                retraceString.context.withMethodContext(element, retraceFrameResult.isAmbiguous()));
-          });
-    }
   }
 
   private static class FieldNameGroup extends RegularExpressionGroup {
 
-    private final boolean printVerbose;
-
-    public FieldNameGroup(boolean printVerbose) {
-      this.printVerbose = printVerbose;
-    }
-
     @Override
     String subExpression() {
       return javaIdentifierSegment;
@@ -767,70 +260,15 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return new QualifiedRegularExpressionGroupHandler() {
-
-        private ClassNameGroupHandler classNameGroupHandler;
-
-        @Override
-        public void setClassNameGroupHandler(ClassNameGroupHandler handler) {
-          classNameGroupHandler = handler;
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
+        if (startOfGroup == NO_MATCH) {
+          return false;
         }
-
-        @Override
-        public List<RetraceString> handleMatch(
-            String original, List<RetraceString> strings, Matcher matcher, RetracerImpl retracer) {
-          final int startOfGroup = matcher.start(captureGroup);
-          if (startOfGroup == NO_MATCH) {
-            if (classNameGroupHandler != null) {
-              for (RetraceString string : strings) {
-                classNameGroupHandler.commitClassName(
-                    original, string, string.context.qualifiedClassContext, matcher);
-              }
-            }
-            return strings;
-          }
-          String fieldName = matcher.group(captureGroup);
-          List<RetraceString> retracedStrings = new ArrayList<>();
-          for (RetraceString retraceString : strings) {
-            if (retraceString.getClassContext() == null) {
-              assert classNameGroupHandler == null;
-              return strings;
-            }
-            final RetraceFieldResult retraceFieldResult =
-                retraceString.getClassContext().lookupField(fieldName);
-            assert !retraceFieldResult.isAmbiguous();
-            retraceFieldResult.forEach(
-                element -> {
-                  if (classNameGroupHandler != null) {
-                    classNameGroupHandler.commitClassName(
-                        original, retraceString, element.getField().getHolderClass(), matcher);
-                  }
-                  retracedStrings.add(
-                      retraceString
-                          .updateContext(
-                              context ->
-                                  context.withQualifiedClassContext(
-                                      element.getField().getHolderClass()))
-                          .appendRetracedString(
-                              original,
-                              getFieldString(element.getField()),
-                              startOfGroup,
-                              matcher.end(captureGroup)));
-                });
-          }
-          return retracedStrings;
-        }
+        builder.registerFieldName(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
-
-    private String getFieldString(RetracedField fieldReference) {
-      if (!printVerbose || fieldReference.isUnknown()) {
-        return fieldReference.getFieldName();
-      }
-      assert fieldReference.isKnown();
-      KnownRetracedField knownRef = fieldReference.asKnown();
-      return knownRef.getFieldType().getTypeName() + " " + fieldReference.getFieldName();
-    }
   }
 
   private static class SourceFileGroup extends RegularExpressionGroup {
@@ -842,40 +280,13 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return (original, strings, matcher, retracer) -> {
-        final int startOfGroup = matcher.start(captureGroup);
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
         if (startOfGroup == NO_MATCH) {
-          return strings;
+          return false;
         }
-        String fileName = matcher.group(captureGroup);
-        List<RetraceString> retracedStrings = null;
-        for (RetraceString retraceString : strings) {
-          if (retraceString.context.classContext == null) {
-            return strings;
-          }
-          if (retracedStrings == null) {
-            retracedStrings = new ArrayList<>();
-          }
-          RetraceSourceFileResult sourceFileResult =
-              retraceString.getMethodContext() != null
-                  ? retraceString
-                      .getMethodContext()
-                      .retraceSourceFile(retraceString.context.qualifiedMethodContext, fileName)
-                  : RetraceUtils.getSourceFile(
-                      retraceString.getClassContext(),
-                      retraceString.context.qualifiedClassContext,
-                      fileName,
-                      retracer);
-          retracedStrings.add(
-              retraceString
-                  .updateContext(context -> context.withSource(sourceFileResult.getFilename()))
-                  .appendRetracedString(
-                      original,
-                      sourceFileResult.getFilename(),
-                      startOfGroup,
-                      matcher.end(captureGroup)));
-        }
-        return retracedStrings;
+        builder.registerSourceFile(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
   }
@@ -889,88 +300,13 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return new RegularExpressionGroupHandler() {
-        @Override
-        public List<RetraceString> handleMatch(
-            String original, List<RetraceString> strings, Matcher matcher, RetracerImpl retracer) {
-          final int startOfGroup = matcher.start(captureGroup);
-          if (startOfGroup == NO_MATCH) {
-            return strings;
-          }
-          String lineNumberAsString = matcher.group(captureGroup);
-          if (lineNumberAsString.isEmpty()) {
-            return strings;
-          }
-          int lineNumber = Integer.parseInt(lineNumberAsString);
-          List<RetraceString> retracedStrings = new ArrayList<>();
-          for (RetraceString retraceString : strings) {
-            Element methodContext = retraceString.context.methodContext;
-            if (methodContext == null) {
-              if (retraceString.context.classContext == null
-                  || retraceString.context.methodName == null) {
-                // We have no way of retracing the line number.
-                retracedStrings.add(retraceString);
-                continue;
-              }
-              // This situation arises when we have a matched pattern as %l..%c.%m where the
-              // line number handler is defined before the methodname handler.
-              MethodNameGroup.retraceMethodForString(
-                  retraceString,
-                  retraceString.context.methodName,
-                  (element, newContext) -> {
-                    // The same method can be represented multiple times if it has multiple
-                    // mappings.
-                    element.visitFrames(
-                        (method, ignoredPosition) -> {
-                          int originalPosition = method.getOriginalPositionOrDefault(lineNumber);
-                          retracedStrings.add(
-                              retraceString
-                                  .duplicate(
-                                      retraceString
-                                          .context
-                                          .withQualifiedMethodContext(method)
-                                          .withLineNumbers(lineNumber, originalPosition))
-                                  .appendRetracedString(
-                                      original,
-                                      originalPosition + "",
-                                      startOfGroup,
-                                      matcher.end(captureGroup)));
-                        });
-                  });
-              continue;
-            }
-            // If the method context is unknown, do nothing.
-            if (methodContext.isUnknown()) {
-              retracedStrings.add(retraceString);
-              continue;
-            }
-            int originalLineNumber =
-                retraceString.context.qualifiedMethodContext.getOriginalPositionOrDefault(
-                    lineNumber);
-            retracedStrings.add(
-                retraceString
-                    .updateContext(
-                        context -> context.withLineNumbers(lineNumber, originalLineNumber))
-                    .appendRetracedString(
-                        original,
-                        originalLineNumber + "",
-                        startOfGroup,
-                        matcher.end(captureGroup)));
-          }
-          return retracedStrings;
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
+        if (startOfGroup == NO_MATCH) {
+          return false;
         }
-
-        @Override
-        public RetraceStringContext buildInitial(
-            RetraceStringContext context, Matcher matcher, RetracerImpl retracer) {
-          if (matcher.start(captureGroup) == NO_MATCH || context.minifiedLineNumber > NO_MATCH) {
-            return context;
-          }
-          String lineNumberAsString = matcher.group(captureGroup);
-          return context.withLineNumbers(
-              lineNumberAsString.isEmpty() ? NO_MATCH : Integer.parseInt(lineNumberAsString),
-              NO_MATCH);
-        }
+        builder.registerLineNumber(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
   }
@@ -1005,31 +341,13 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return (original, strings, matcher, retracer) -> {
-        final int startOfGroup = matcher.start(captureGroup);
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
         if (startOfGroup == NO_MATCH) {
-          return strings;
+          return false;
         }
-        String typeName = matcher.group(captureGroup);
-        String descriptor = DescriptorUtils.javaTypeToDescriptor(typeName);
-        if (!DescriptorUtils.isDescriptor(descriptor) && !"V".equals(descriptor)) {
-          return strings;
-        }
-        TypeReference typeReference = Reference.returnTypeFromDescriptor(descriptor);
-        RetraceTypeResultImpl retracedType = retracer.retraceType(typeReference);
-        assert !retracedType.isAmbiguous();
-        for (RetraceString retraceString : strings) {
-          retracedType.forEach(
-              element -> {
-                RetracedTypeImpl retracedReference = element.getType();
-                retraceString.appendRetracedString(
-                    original,
-                    retracedReference.isVoid() ? "void" : retracedReference.getTypeName(),
-                    startOfGroup,
-                    matcher.end(captureGroup));
-              });
-        }
-        return strings;
+        builder.registerFieldOrReturnType(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
   }
@@ -1043,37 +361,15 @@
 
     @Override
     RegularExpressionGroupHandler createHandler(String captureGroup) {
-      return (original, strings, matcher, retracer) -> {
-        final int startOfGroup = matcher.start(captureGroup);
+      return (builder, matcher) -> {
+        int startOfGroup = matcher.start(captureGroup);
         if (startOfGroup == NO_MATCH) {
-          return strings;
+          return false;
         }
-        final String formals =
-            Arrays.stream(matcher.group(captureGroup).split(","))
-                .map(
-                    typeName -> {
-                      typeName = typeName.trim();
-                      if (typeName.isEmpty()) {
-                        return null;
-                      }
-                      String descriptor = DescriptorUtils.javaTypeToDescriptor(typeName);
-                      if (!DescriptorUtils.isDescriptor(descriptor) && !"V".equals(descriptor)) {
-                        return typeName;
-                      }
-                      final RetraceTypeResultImpl retraceResult =
-                          retracer.retraceType(Reference.returnTypeFromDescriptor(descriptor));
-                      assert !retraceResult.isAmbiguous();
-                      final Box<RetracedTypeImpl> elementBox = new Box<>();
-                      retraceResult.forEach(element -> elementBox.set(element.getType()));
-                      return elementBox.get().getTypeName();
-                    })
-                .filter(Objects::nonNull)
-                .collect(Collectors.joining(","));
-        for (RetraceString string : strings) {
-          string.appendRetracedString(original, formals, startOfGroup, matcher.end(captureGroup));
-        }
-        return strings;
+        builder.registerMethodArguments(startOfGroup, matcher.end(captureGroup));
+        return true;
       };
     }
   }
 }
+
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetracedMethodImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetracedMethodImpl.java
index 998fe7d..1f4ad68 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetracedMethodImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetracedMethodImpl.java
@@ -8,6 +8,8 @@
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.retrace.RetracedMethod;
+import com.android.tools.r8.utils.ComparatorUtils;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -34,6 +36,27 @@
     return null;
   }
 
+  @Override
+  public int compareTo(RetracedMethod other) {
+    return Comparator.comparing(RetracedMethod::getMethodName)
+        .thenComparing(RetracedMethod::isKnown)
+        .thenComparing(
+            RetracedMethod::asKnown,
+            Comparator.nullsFirst(
+                    Comparator.comparing(
+                        (KnownRetracedMethod m) -> {
+                          if (m == null) {
+                            return null;
+                          }
+                          return m.isVoid() ? "void" : m.getReturnType().getTypeName();
+                        }))
+                .thenComparing(
+                    KnownRetracedMethod::getFormalTypes,
+                    ComparatorUtils.listComparator(
+                        Comparator.comparing(TypeReference::getTypeName))))
+        .compare(this, other);
+  }
+
   public static final class KnownRetracedMethodImpl extends RetracedMethodImpl
       implements KnownRetracedMethod {
 
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/RetracedTypeImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/RetracedTypeImpl.java
index 8162e05..d467cc1 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/RetracedTypeImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/RetracedTypeImpl.java
@@ -23,6 +23,10 @@
     return new RetracedTypeImpl(typeReference);
   }
 
+  static RetracedType createVoid() {
+    return new RetracedTypeImpl(null);
+  }
+
   @Override
   public boolean isVoid() {
     return typeReference == null;
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
index c21b92d..a949fc1 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementProxyRetracerImpl.java
@@ -5,13 +5,23 @@
 package com.android.tools.r8.retrace.internal;
 
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.retrace.RetraceClassResult;
+import com.android.tools.r8.retrace.RetraceFieldResult;
 import com.android.tools.r8.retrace.RetraceStackTraceProxy;
 import com.android.tools.r8.retrace.RetracedClass;
+import com.android.tools.r8.retrace.RetracedField;
 import com.android.tools.r8.retrace.RetracedMethod;
+import com.android.tools.r8.retrace.RetracedType;
 import com.android.tools.r8.retrace.StackTraceElementProxy;
 import com.android.tools.r8.retrace.StackTraceElementProxyRetracer;
+import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.ListUtils;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 public class StackTraceElementProxyRetracerImpl<T extends StackTraceElementProxy<?>>
@@ -28,47 +38,188 @@
     if (!element.hasClassName()) {
       return Stream.of(RetraceStackTraceProxyImpl.builder(element).build());
     }
-    RetraceClassResultImpl classResult =
-        retracer.retraceClass(Reference.classFromTypeName(element.className()));
-    List<RetraceStackTraceProxyImpl<T>> retracedProxies = new ArrayList<>();
-    if (!element.hasMethodName()) {
-      classResult.forEach(
-          classElement -> {
-            RetraceStackTraceProxyImpl.Builder<T> proxy =
-                RetraceStackTraceProxyImpl.builder(element)
-                    .setRetracedClassElement(classElement.getRetracedClass())
-                    .setAmbiguous(classResult.isAmbiguous());
-            if (element.hasFileName()) {
-              proxy.setSourceFile(classElement.retraceSourceFile(element.fileName()).getFilename());
-            }
-            retracedProxies.add(proxy.build());
-          });
+    RetraceClassResultImpl classResult = retracer.retraceClass(element.getClassReference());
+    if (element.hasMethodName()) {
+      return retraceMethod(element, classResult);
+    } else if (element.hasFieldName()) {
+      return retraceField(element, classResult);
     } else {
-      RetraceFrameResultImpl frameResult =
-          element.hasLineNumber()
-              ? classResult.lookupFrame(element.methodName(), element.lineNumber())
-              : classResult.lookupFrame(element.methodName());
-      frameResult.forEach(
-          frameElement -> {
-            frameElement.visitFrames(
-                (frame, index) -> {
-                  RetraceStackTraceProxyImpl.Builder<T> proxy =
-                      RetraceStackTraceProxyImpl.builder(element)
-                          .setRetracedClassElement(frame.getHolderClass())
-                          .setRetracedMethodElement(frame)
-                          .setAmbiguous(frameResult.isAmbiguous() && index == 0);
-                  if (element.hasLineNumber()) {
-                    proxy.setLineNumber(frame.getOriginalPositionOrDefault(element.lineNumber()));
-                  }
-                  if (element.hasFileName()) {
-                    proxy.setSourceFile(
-                        frameElement.retraceSourceFile(frame, element.fileName()).getFilename());
-                  }
-                  retracedProxies.add(proxy.build());
-                });
-          });
+      return retraceClassOrType(element, classResult);
     }
-    return retracedProxies.stream();
+  }
+
+  private Stream<RetraceStackTraceProxyImpl<T>> retraceClassOrType(
+      T element, RetraceClassResult classResult) {
+    return classResult.stream()
+        .flatMap(
+            classElement ->
+                retraceFieldOrReturnType(element)
+                    .flatMap(
+                        fieldOrReturnTypeConsumer ->
+                            retracedMethodArguments(element)
+                                .map(
+                                    argumentsConsumer -> {
+                                      RetraceStackTraceProxyImpl.Builder<T> proxy =
+                                          RetraceStackTraceProxyImpl.builder(element)
+                                              .setRetracedClass(classElement.getRetracedClass())
+                                              .setAmbiguous(classResult.isAmbiguous())
+                                              .setTopFrame(true);
+                                      argumentsConsumer.accept(proxy);
+                                      fieldOrReturnTypeConsumer.accept(proxy);
+                                      if (element.hasFileName()) {
+                                        proxy.setSourceFile(
+                                            classElement
+                                                .retraceSourceFile(element.getFileName())
+                                                .getFilename());
+                                      }
+                                      return proxy.build();
+                                    })));
+  }
+
+  private Stream<RetraceStackTraceProxyImpl<T>> retraceMethod(
+      T element, RetraceClassResultImpl classResult) {
+    return retraceFieldOrReturnType(element)
+        .flatMap(
+            fieldOrReturnTypeConsumer ->
+                retracedMethodArguments(element)
+                    .flatMap(
+                        argumentsConsumer -> {
+                          RetraceFrameResultImpl frameResult =
+                              element.hasLineNumber()
+                                  ? classResult.lookupFrame(
+                                      element.getMethodName(), element.getLineNumber())
+                                  : classResult.lookupFrame(element.getMethodName());
+                          return frameResult.stream()
+                              .flatMap(
+                                  frameElement -> {
+                                    List<RetraceStackTraceProxyImpl<T>> retracedProxies =
+                                        new ArrayList<>();
+                                    frameElement.visitFrames(
+                                        (frame, index) -> {
+                                          RetraceStackTraceProxyImpl.Builder<T> proxy =
+                                              RetraceStackTraceProxyImpl.builder(element)
+                                                  .setRetracedClass(frame.getHolderClass())
+                                                  .setRetracedMethod(frame)
+                                                  .setAmbiguous(
+                                                      frameResult.isAmbiguous() && index == 0)
+                                                  .setTopFrame(index == 0);
+                                          if (element.hasLineNumber()) {
+                                            proxy.setLineNumber(
+                                                frame.getOriginalPositionOrDefault(
+                                                    element.getLineNumber()));
+                                          }
+                                          if (element.hasFileName()) {
+                                            proxy.setSourceFile(
+                                                frameElement
+                                                    .retraceSourceFile(frame, element.getFileName())
+                                                    .getFilename());
+                                          }
+                                          fieldOrReturnTypeConsumer.accept(proxy);
+                                          argumentsConsumer.accept(proxy);
+                                          retracedProxies.add(proxy.build());
+                                        });
+                                    return retracedProxies.stream();
+                                  });
+                        }));
+  }
+
+  private Stream<RetraceStackTraceProxyImpl<T>> retraceField(
+      T element, RetraceClassResult classResult) {
+    return retraceFieldOrReturnType(element)
+        .flatMap(
+            fieldOrReturnTypeConsumer ->
+                retracedMethodArguments(element)
+                    .flatMap(
+                        argumentsConsumer -> {
+                          RetraceFieldResult retraceFieldResult =
+                              classResult.lookupField(element.getFieldName());
+                          return retraceFieldResult.stream()
+                              .map(
+                                  fieldElement -> {
+                                    RetraceStackTraceProxyImpl.Builder<T> proxy =
+                                        RetraceStackTraceProxyImpl.builder(element)
+                                            .setRetracedClass(
+                                                fieldElement.getField().getHolderClass())
+                                            .setRetracedField(fieldElement.getField())
+                                            .setAmbiguous(retraceFieldResult.isAmbiguous())
+                                            .setTopFrame(true);
+                                    if (element.hasFileName()) {
+                                      proxy.setSourceFile(
+                                          fieldElement
+                                              .retraceSourceFile(element.getFileName())
+                                              .getFilename());
+                                    }
+                                    fieldOrReturnTypeConsumer.accept(proxy);
+                                    argumentsConsumer.accept(proxy);
+                                    return proxy.build();
+                                  });
+                        }));
+  }
+
+  private Stream<Consumer<RetraceStackTraceProxyImpl.Builder<T>>> retraceFieldOrReturnType(
+      T element) {
+    if (!element.hasFieldOrReturnType()) {
+      return Stream.of(noEffect -> {});
+    }
+    String elementOrReturnType = element.getFieldOrReturnType();
+    if (elementOrReturnType.equals("void")) {
+      return Stream.of(proxy -> proxy.setRetracedFieldOrReturnType(RetracedTypeImpl.createVoid()));
+    } else {
+      TypeReference typeReference = Reference.typeFromTypeName(elementOrReturnType);
+      RetraceTypeResultImpl retraceTypeResult = retracer.retraceType(typeReference);
+      return retraceTypeResult.stream()
+          .map(
+              type ->
+                  (proxy -> {
+                    proxy.setRetracedFieldOrReturnType(type.getType());
+                    if (retraceTypeResult.isAmbiguous()) {
+                      proxy.setAmbiguous(true);
+                    }
+                  }));
+    }
+  }
+
+  private Stream<Consumer<RetraceStackTraceProxyImpl.Builder<T>>> retracedMethodArguments(
+      T element) {
+    if (!element.hasMethodArguments()) {
+      return Stream.of(noEffect -> {});
+    }
+    List<RetraceTypeResultImpl> retracedResults =
+        Arrays.stream(element.getMethodArguments().split(","))
+            .map(typeName -> retracer.retraceType(Reference.typeFromTypeName(typeName)))
+            .collect(Collectors.toList());
+    List<List<RetracedType>> initial = new ArrayList<>();
+    initial.add(new ArrayList<>());
+    Box<Boolean> isAmbiguous = new Box<>(false);
+    List<List<RetracedType>> retracedArguments =
+        ListUtils.fold(
+            retracedResults,
+            initial,
+            (acc, retracedTypeResult) -> {
+              if (retracedTypeResult.isAmbiguous()) {
+                isAmbiguous.set(true);
+              }
+              List<List<RetracedType>> newResult = new ArrayList<>();
+              retracedTypeResult.forEach(
+                  retracedElement -> {
+                    acc.forEach(
+                        oldResult -> {
+                          List<RetracedType> newList = new ArrayList<>(oldResult);
+                          newList.add(retracedElement.getType());
+                          newResult.add(newList);
+                        });
+                  });
+              return newResult;
+            });
+    return retracedArguments.stream()
+        .map(
+            arguments ->
+                proxy -> {
+                  proxy.setRetracedMethodArguments(arguments);
+                  if (isAmbiguous.get()) {
+                    proxy.setAmbiguous(true);
+                  }
+                });
   }
 
   public static class RetraceStackTraceProxyImpl<T extends StackTraceElementProxy<?>>
@@ -77,24 +228,36 @@
     private final T originalItem;
     private final RetracedClass retracedClass;
     private final RetracedMethod retracedMethod;
+    private final RetracedField retracedField;
+    private final RetracedType fieldOrReturnType;
+    private final List<RetracedType> methodArguments;
     private final String sourceFile;
     private final int lineNumber;
     private final boolean isAmbiguous;
+    private final boolean isTopFrame;
 
     private RetraceStackTraceProxyImpl(
         T originalItem,
         RetracedClass retracedClass,
         RetracedMethod retracedMethod,
+        RetracedField retracedField,
+        RetracedType fieldOrReturnType,
+        List<RetracedType> methodArguments,
         String sourceFile,
         int lineNumber,
-        boolean isAmbiguous) {
+        boolean isAmbiguous,
+        boolean isTopFrame) {
       assert originalItem != null;
       this.originalItem = originalItem;
       this.retracedClass = retracedClass;
       this.retracedMethod = retracedMethod;
+      this.retracedField = retracedField;
+      this.fieldOrReturnType = fieldOrReturnType;
+      this.methodArguments = methodArguments;
       this.sourceFile = sourceFile;
       this.lineNumber = lineNumber;
       this.isAmbiguous = isAmbiguous;
+      this.isTopFrame = isTopFrame;
     }
 
     @Override
@@ -103,6 +266,11 @@
     }
 
     @Override
+    public boolean isTopFrame() {
+      return isTopFrame;
+    }
+
+    @Override
     public boolean hasRetracedClass() {
       return retracedClass != null;
     }
@@ -113,6 +281,11 @@
     }
 
     @Override
+    public boolean hasRetracedField() {
+      return retracedField != null;
+    }
+
+    @Override
     public boolean hasSourceFile() {
       return sourceFile != null;
     }
@@ -123,6 +296,16 @@
     }
 
     @Override
+    public boolean hasFieldOrReturnType() {
+      return fieldOrReturnType != null;
+    }
+
+    @Override
+    public boolean hasMethodArguments() {
+      return methodArguments != null;
+    }
+
+    @Override
     public T getOriginalItem() {
       return originalItem;
     }
@@ -138,6 +321,21 @@
     }
 
     @Override
+    public RetracedField getRetracedField() {
+      return retracedField;
+    }
+
+    @Override
+    public RetracedType getRetracedFieldOrReturnType() {
+      return fieldOrReturnType;
+    }
+
+    @Override
+    public List<RetracedType> getMethodArguments() {
+      return methodArguments;
+    }
+
+    @Override
     public String getSourceFile() {
       return sourceFile;
     }
@@ -151,29 +349,91 @@
       return lineNumber;
     }
 
+    @Override
+    public int compareTo(RetraceStackTraceProxy<T> other) {
+      int classCompare = Boolean.compare(hasRetracedClass(), other.hasRetracedClass());
+      if (classCompare != 0) {
+        return classCompare;
+      }
+      if (hasRetracedClass()) {
+        classCompare =
+            getRetracedClass().getTypeName().compareTo(other.getRetracedClass().getTypeName());
+        if (classCompare != 0) {
+          return classCompare;
+        }
+      }
+      int methodCompare = Boolean.compare(hasRetracedMethod(), other.hasRetracedMethod());
+      if (methodCompare != 0) {
+        return methodCompare;
+      }
+      if (hasRetracedMethod()) {
+        methodCompare = getRetracedMethod().compareTo(other.getRetracedMethod());
+        if (methodCompare != 0) {
+          return methodCompare;
+        }
+      }
+      int sourceFileCompare = Boolean.compare(hasSourceFile(), other.hasSourceFile());
+      if (sourceFileCompare != 0) {
+        return sourceFileCompare;
+      }
+      if (hasSourceFile()) {
+        sourceFileCompare = getSourceFile().compareTo(other.getSourceFile());
+        if (sourceFileCompare != 0) {
+          return sourceFileCompare;
+        }
+      }
+      int lineNumberCompare = Boolean.compare(hasLineNumber(), other.hasLineNumber());
+      if (lineNumberCompare != 0) {
+        return lineNumberCompare;
+      }
+      if (hasLineNumber()) {
+        return Integer.compare(lineNumber, other.getLineNumber());
+      }
+      return 0;
+    }
+
     private static class Builder<T extends StackTraceElementProxy<?>> {
 
       private final T originalElement;
       private RetracedClass classContext;
       private RetracedMethod methodContext;
+      private RetracedField retracedField;
+      private RetracedType fieldOrReturnType;
+      private List<RetracedType> methodArguments;
       private String sourceFile;
       private int lineNumber = -1;
       private boolean isAmbiguous;
+      private boolean isTopFrame;
 
       private Builder(T originalElement) {
         this.originalElement = originalElement;
       }
 
-      private Builder<T> setRetracedClassElement(RetracedClass retracedClass) {
+      private Builder<T> setRetracedClass(RetracedClass retracedClass) {
         this.classContext = retracedClass;
         return this;
       }
 
-      private Builder<T> setRetracedMethodElement(RetracedMethod methodElement) {
+      private Builder<T> setRetracedMethod(RetracedMethod methodElement) {
         this.methodContext = methodElement;
         return this;
       }
 
+      private Builder<T> setRetracedField(RetracedField retracedField) {
+        this.retracedField = retracedField;
+        return this;
+      }
+
+      private Builder<T> setRetracedFieldOrReturnType(RetracedType retracedType) {
+        this.fieldOrReturnType = retracedType;
+        return this;
+      }
+
+      private Builder<T> setRetracedMethodArguments(List<RetracedType> arguments) {
+        this.methodArguments = arguments;
+        return this;
+      }
+
       private Builder<T> setSourceFile(String sourceFile) {
         this.sourceFile = sourceFile;
         return this;
@@ -189,6 +449,11 @@
         return this;
       }
 
+      private Builder<T> setTopFrame(boolean topFrame) {
+        isTopFrame = topFrame;
+        return this;
+      }
+
       private RetraceStackTraceProxyImpl<T> build() {
         RetracedClass retracedClass = classContext;
         if (methodContext != null) {
@@ -198,9 +463,13 @@
             originalElement,
             retracedClass,
             methodContext,
-            sourceFile != null ? sourceFile : null,
+            retracedField,
+            fieldOrReturnType,
+            methodArguments,
+            sourceFile,
             lineNumber,
-            isAmbiguous);
+            isAmbiguous,
+            isTopFrame);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
index 202d297..f649598 100644
--- a/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
+++ b/src/main/java/com/android/tools/r8/retrace/internal/StackTraceElementStringProxy.java
@@ -5,36 +5,53 @@
 package com.android.tools.r8.retrace.internal;
 
 import static com.android.tools.r8.retrace.internal.PlainStackTraceVisitor.firstNonWhiteSpaceCharacterFromIndex;
+import static com.android.tools.r8.retrace.internal.RetraceUtils.methodDescriptionFromRetraceMethod;
 import static com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.StringIndex.noIndex;
 
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.retrace.RetracedClass;
+import com.android.tools.r8.retrace.RetracedField;
+import com.android.tools.r8.retrace.RetracedType;
 import com.android.tools.r8.retrace.StackTraceElementProxy;
 import com.android.tools.r8.retrace.internal.StackTraceElementProxyRetracerImpl.RetraceStackTraceProxyImpl;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.android.tools.r8.utils.TriFunction;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.function.BiFunction;
 
 public final class StackTraceElementStringProxy extends StackTraceElementProxy<String> {
 
   private final String line;
   private final List<StringIndex> orderedIndices;
-  private final StringIndex className;
+  private final ClassStringIndex className;
   private final StringIndex methodName;
   private final StringIndex sourceFile;
   private final StringIndex lineNumber;
+  private final StringIndex fieldName;
+  private final StringIndex fieldOrReturnType;
+  private final StringIndex methodArguments;
 
   private StackTraceElementStringProxy(
       String line,
       List<StringIndex> orderedIndices,
-      StringIndex className,
+      ClassStringIndex className,
       StringIndex methodName,
       StringIndex sourceFile,
-      StringIndex lineNumber) {
+      StringIndex lineNumber,
+      StringIndex fieldName,
+      StringIndex fieldOrReturnType,
+      StringIndex methodArguments) {
     this.line = line;
     this.orderedIndices = orderedIndices;
     this.className = className;
     this.methodName = methodName;
     this.sourceFile = sourceFile;
     this.lineNumber = lineNumber;
+    this.fieldName = fieldName;
+    this.fieldOrReturnType = fieldOrReturnType;
+    this.methodArguments = methodArguments;
   }
 
   static StackTraceElementStringProxyBuilder builder(String line) {
@@ -62,22 +79,37 @@
   }
 
   @Override
-  public String className() {
-    return hasClassName() ? getEntryInLine(className) : null;
+  public boolean hasFieldName() {
+    return fieldName.hasIndex();
   }
 
   @Override
-  public String methodName() {
+  public boolean hasFieldOrReturnType() {
+    return fieldOrReturnType.hasIndex();
+  }
+
+  @Override
+  public boolean hasMethodArguments() {
+    return methodArguments.hasIndex();
+  }
+
+  @Override
+  public ClassReference getClassReference() {
+    return hasClassName() ? className.getReference(line) : null;
+  }
+
+  @Override
+  public String getMethodName() {
     return hasMethodName() ? getEntryInLine(methodName) : null;
   }
 
   @Override
-  public String fileName() {
+  public String getFileName() {
     return hasFileName() ? getEntryInLine(sourceFile) : null;
   }
 
   @Override
-  public int lineNumber() {
+  public int getLineNumber() {
     if (!hasLineNumber()) {
       return -1;
     }
@@ -88,9 +120,25 @@
     }
   }
 
+  @Override
+  public String getFieldName() {
+    return hasFieldName() ? getEntryInLine(fieldName) : null;
+  }
+
+  @Override
+  public String getFieldOrReturnType() {
+    return hasFieldOrReturnType() ? getEntryInLine(fieldOrReturnType) : null;
+  }
+
+  @Override
+  public String getMethodArguments() {
+    return hasMethodArguments() ? getEntryInLine(methodArguments) : null;
+  }
+
   public String toRetracedItem(
       RetraceStackTraceProxyImpl<StackTraceElementStringProxy> retracedProxy,
-      boolean printAmbiguous) {
+      boolean printAmbiguous,
+      boolean verbose) {
     StringBuilder sb = new StringBuilder();
     int lastSeenIndex = 0;
     if (retracedProxy.isAmbiguous() && printAmbiguous) {
@@ -100,7 +148,7 @@
     }
     for (StringIndex index : orderedIndices) {
       sb.append(line, lastSeenIndex, index.startIndex);
-      sb.append(index.retracedString.apply(retracedProxy, this));
+      sb.append(index.retracedString.apply(retracedProxy, this, verbose));
       lastSeenIndex = index.endIndex;
     }
     sb.append(line, lastSeenIndex, line.length());
@@ -116,30 +164,43 @@
     return line.substring(index.startIndex, index.endIndex);
   }
 
+  public enum ClassNameType {
+    BINARY,
+    TYPENAME
+  }
+
   public static class StackTraceElementStringProxyBuilder {
 
     private final String line;
     private final List<StringIndex> orderedIndices = new ArrayList<>();
-    private StringIndex className = noIndex();
+    private ClassStringIndex className = noIndex();
     private StringIndex methodName = noIndex();
     private StringIndex sourceFile = noIndex();
     private StringIndex lineNumber = noIndex();
+    private StringIndex fieldName = noIndex();
+    private StringIndex fieldOrReturnType = noIndex();
+    private StringIndex methodArguments = noIndex();
     private int lastSeenStartIndex = -1;
 
     private StackTraceElementStringProxyBuilder(String line) {
       this.line = line;
     }
 
-    public StackTraceElementStringProxyBuilder registerClassName(int startIndex, int endIndex) {
+    public StackTraceElementStringProxyBuilder registerClassName(
+        int startIndex, int endIndex, ClassNameType classNameType) {
       ensureLineIndexIncreases(startIndex);
       className =
-          new StringIndex(
+          new ClassStringIndex(
               startIndex,
               endIndex,
-              (retraced, original) -> {
+              (retraced, original, verbose) -> {
                 assert retraced.hasRetracedClass();
-                return retraced.getRetracedClass().getTypeName();
-              });
+                RetracedClass retracedClass = retraced.getRetracedClass();
+                return classNameType == ClassNameType.BINARY
+                    ? retracedClass.getBinaryName()
+                    : retracedClass.getTypeName();
+              },
+              classNameType);
       orderedIndices.add(className);
       return this;
     }
@@ -149,10 +210,13 @@
           new StringIndex(
               startIndex,
               endIndex,
-              (retraced, original) ->
-                  retraced.hasRetracedMethod()
-                      ? retraced.getRetracedMethod().getMethodName()
-                      : original.methodName());
+              (retraced, original, verbose) -> {
+                if (!retraced.hasRetracedMethod()) {
+                  return original.getMethodName();
+                }
+                return methodDescriptionFromRetraceMethod(
+                    retraced.getRetracedMethod(), false, verbose);
+              });
       orderedIndices.add(methodName);
       return this;
     }
@@ -162,8 +226,8 @@
           new StringIndex(
               startIndex,
               endIndex,
-              (retraced, original) ->
-                  retraced.hasSourceFile() ? retraced.getSourceFile() : original.fileName());
+              (retraced, original, verbose) ->
+                  retraced.hasSourceFile() ? retraced.getSourceFile() : original.getFileName());
       orderedIndices.add(sourceFile);
       return this;
     }
@@ -173,7 +237,7 @@
           new StringIndex(
               startIndex,
               endIndex,
-              (retraced, original) ->
+              (retraced, original, verbose) ->
                   retraced.hasLineNumber()
                       ? retraced.getLineNumber() + ""
                       : original.lineNumberAsString());
@@ -181,9 +245,73 @@
       return this;
     }
 
+    public StackTraceElementStringProxyBuilder registerFieldName(int startIndex, int endIndex) {
+      fieldName =
+          new StringIndex(
+              startIndex,
+              endIndex,
+              (retraced, original, verbose) -> {
+                if (!retraced.hasRetracedField()) {
+                  return original.getFieldName();
+                }
+                RetracedField retracedField = retraced.getRetracedField();
+                if (!verbose || retracedField.isUnknown()) {
+                  return retracedField.getFieldName();
+                }
+                return retracedField.asKnown().getFieldType().getTypeName()
+                    + " "
+                    + retracedField.getFieldName();
+              });
+      orderedIndices.add(fieldName);
+      return this;
+    }
+
+    public StackTraceElementStringProxyBuilder registerFieldOrReturnType(
+        int startIndex, int endIndex) {
+      fieldOrReturnType =
+          new StringIndex(
+              startIndex,
+              endIndex,
+              (retraced, original, verbose) -> {
+                if (!retraced.hasFieldOrReturnType()) {
+                  return original.getFieldOrReturnType();
+                }
+                return retraced.getRetracedFieldOrReturnType().isVoid()
+                    ? "void"
+                    : retraced.getRetracedFieldOrReturnType().getTypeName();
+              });
+      orderedIndices.add(fieldOrReturnType);
+      return this;
+    }
+
+    public StackTraceElementStringProxyBuilder registerMethodArguments(
+        int startIndex, int endIndex) {
+      methodArguments =
+          new StringIndex(
+              startIndex,
+              endIndex,
+              (retraced, original, verbose) -> {
+                if (!retraced.hasMethodArguments()) {
+                  return original.getMethodArguments();
+                }
+                return StringUtils.join(
+                    retraced.getMethodArguments(), ",", BraceType.NONE, RetracedType::getTypeName);
+              });
+      orderedIndices.add(methodArguments);
+      return this;
+    }
+
     public StackTraceElementStringProxy build() {
       return new StackTraceElementStringProxy(
-          line, orderedIndices, className, methodName, sourceFile, lineNumber);
+          line,
+          orderedIndices,
+          className,
+          methodName,
+          sourceFile,
+          lineNumber,
+          fieldName,
+          fieldOrReturnType,
+          methodArguments);
     }
 
     private void ensureLineIndexIncreases(int newStartIndex) {
@@ -194,28 +322,31 @@
     }
   }
 
-  static final class StringIndex {
+  static class StringIndex {
 
-    private static final StringIndex NO_INDEX = new StringIndex(-1, -1, null);
+    private static final ClassStringIndex NO_INDEX =
+        new ClassStringIndex(-1, -1, null, ClassNameType.TYPENAME);
 
-    static StringIndex noIndex() {
+    static ClassStringIndex noIndex() {
       return NO_INDEX;
     }
 
-    private final int startIndex;
-    private final int endIndex;
-    private final BiFunction<
+    protected final int startIndex;
+    protected final int endIndex;
+    private final TriFunction<
             RetraceStackTraceProxyImpl<StackTraceElementStringProxy>,
             StackTraceElementStringProxy,
+            Boolean,
             String>
         retracedString;
 
     private StringIndex(
         int startIndex,
         int endIndex,
-        BiFunction<
+        TriFunction<
                 RetraceStackTraceProxyImpl<StackTraceElementStringProxy>,
                 StackTraceElementStringProxy,
+                Boolean,
                 String>
             retracedString) {
       this.startIndex = startIndex;
@@ -227,4 +358,30 @@
       return this != NO_INDEX;
     }
   }
+
+  static final class ClassStringIndex extends StringIndex {
+
+    private final ClassNameType classNameType;
+
+    private ClassStringIndex(
+        int startIndex,
+        int endIndex,
+        TriFunction<
+                RetraceStackTraceProxyImpl<StackTraceElementStringProxy>,
+                StackTraceElementStringProxy,
+                Boolean,
+                String>
+            retracedString,
+        ClassNameType classNameType) {
+      super(startIndex, endIndex, retracedString);
+      this.classNameType = classNameType;
+    }
+
+    ClassReference getReference(String line) {
+      String className = line.substring(startIndex, endIndex);
+      return classNameType == ClassNameType.BINARY
+          ? Reference.classFromBinaryName(className)
+          : Reference.classFromTypeName(className);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 46e4629..fc6cb56 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
@@ -27,6 +28,7 @@
 public class AnnotationRemover {
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
   private final Set<DexAnnotation> annotationsToRetain;
   private final Set<DexType> classesToRetainInnerClassAttributeFor;
   private final ProguardKeepAttributes keep;
@@ -38,6 +40,7 @@
       Set<DexAnnotation> annotationsToRetain,
       Set<DexType> removedClasses) {
     this.appView = appView;
+    this.options = appView.options();
     this.annotationsToRetain = annotationsToRetain;
     this.classesToRetainInnerClassAttributeFor = classesToRetainInnerClassAttributeFor;
     this.keep = appView.options().getProguardConfiguration().getKeepAttributes();
@@ -187,25 +190,31 @@
       stripAttributes(clazz);
       clazz.setAnnotations(
           clazz.annotations().rewrite(annotation -> rewriteAnnotation(clazz, annotation)));
-      clazz.forEachMethod(this::processMethod);
-      clazz.forEachField(this::processField);
+      clazz.forEachMethod(method -> processMethod(method, clazz));
+      clazz.forEachField(field -> processField(field, clazz));
     }
   }
 
-  private void processMethod(DexEncodedMethod method) {
+  private void processMethod(DexEncodedMethod method, DexProgramClass clazz) {
     method.setAnnotations(
         method.annotations().rewrite(annotation -> rewriteAnnotation(method, annotation)));
     method.parameterAnnotationsList =
         method.parameterAnnotationsList.keepIf(this::filterParameterAnnotations);
-    if (!keep.signature) {
+    if (appView
+        .getKeepInfo()
+        .getMethodInfo(method, clazz)
+        .isAllowSignatureAttributeRemovalAllowed(options)) {
       method.clearGenericSignature();
     }
   }
 
-  private void processField(DexEncodedField field) {
+  private void processField(DexEncodedField field, DexProgramClass clazz) {
     field.setAnnotations(
         field.annotations().rewrite(annotation -> rewriteAnnotation(field, annotation)));
-    if (!keep.signature) {
+    if (appView
+        .getKeepInfo()
+        .getFieldInfo(field, clazz)
+        .isAllowSignatureAttributeRemovalAllowed(options)) {
       field.clearGenericSignature();
     }
   }
@@ -321,8 +330,10 @@
       clazz.clearEnclosingMethodAttribute();
       clazz.clearInnerClasses();
     }
-    // TODO(b/170077516): Prune attributes.
-    if (!keep.signature) {
+    if (appView
+        .getKeepInfo()
+        .getClassInfo(clazz)
+        .isAllowSignatureAttributeRemovalAllowed(options)) {
       clazz.clearClassSignature();
     }
   }
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 7c6d911..b54a0db 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -19,6 +19,7 @@
 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.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
@@ -136,9 +137,9 @@
   /** All items with assumemayhavesideeffects rule. */
   public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
   /** All items with assumenosideeffects rule. */
-  public final Map<DexReference, ProguardMemberRule> noSideEffects;
+  public final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects;
   /** All items with assumevalues rule. */
-  public final Map<DexReference, ProguardMemberRule> assumedValues;
+  public final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues;
   /** All methods that should be inlined if possible due to a configuration directive. */
   public final Set<DexMethod> alwaysInline;
   /** All methods that *must* be inlined due to a configuration directive (testing only). */
@@ -213,8 +214,8 @@
       Map<DexCallSite, ProgramMethodSet> callSites,
       KeepInfoCollection keepInfo,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-      Map<DexReference, ProguardMemberRule> noSideEffects,
-      Map<DexReference, ProguardMemberRule> assumedValues,
+      Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+      Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
       Set<DexMethod> alwaysInline,
       Set<DexMethod> forceInline,
       Set<DexMethod> neverInline,
@@ -294,8 +295,8 @@
       Map<DexCallSite, ProgramMethodSet> callSites,
       KeepInfoCollection keepInfo,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-      Map<DexReference, ProguardMemberRule> noSideEffects,
-      Map<DexReference, ProguardMemberRule> assumedValues,
+      Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+      Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
       Set<DexMethod> alwaysInline,
       Set<DexMethod> forceInline,
       Set<DexMethod> neverInline,
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 a7fdc85..be79780 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -57,6 +57,7 @@
 import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMember;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.ResolutionResult.FailedResolutionResult;
@@ -93,6 +94,7 @@
 import com.android.tools.r8.shaking.KeepInfoCollection.MutableKeepInfoCollection;
 import com.android.tools.r8.shaking.RootSetBuilder.ConsequentRootSet;
 import com.android.tools.r8.shaking.RootSetBuilder.ItemsWithRules;
+import com.android.tools.r8.shaking.RootSetBuilder.MutableItemsWithRules;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
 import com.android.tools.r8.utils.Action;
@@ -126,6 +128,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -2125,9 +2128,10 @@
     worklist.addIfNotSeen(interfaces);
     // First we lookup and mark all targets on the instantiated class for each reachable method in
     // the super chain (inclusive).
+    DexClass initialClass = clazz;
     while (clazz != null) {
       if (clazz.isProgramClass()) {
-        markProgramMethodOverridesAsLive(instantiation, clazz.asProgramClass(), seen);
+        markProgramMethodOverridesAsLive(instantiation, initialClass, clazz.asProgramClass(), seen);
       } else {
         markLibraryAndClasspathMethodOverridesAsLive(instantiation, clazz);
       }
@@ -2148,7 +2152,7 @@
       if (iface.isNotProgramClass()) {
         markLibraryAndClasspathMethodOverridesAsLive(instantiation, iface);
       } else {
-        markProgramMethodOverridesAsLive(instantiation, iface.asProgramClass(), seen);
+        markProgramMethodOverridesAsLive(instantiation, initialClass, iface.asProgramClass(), seen);
       }
       worklist.addIfNotSeen(Arrays.asList(iface.interfaces.values));
     }
@@ -2160,18 +2164,28 @@
 
   private void markProgramMethodOverridesAsLive(
       InstantiatedObject instantiation,
+      DexClass initialClass,
       DexProgramClass superClass,
       Set<Wrapper<DexMethod>> seenMethods) {
     for (DexMethod method : getReachableVirtualTargets(superClass)) {
       assert method.holder == superClass.type;
-      if (seenMethods.add(MethodSignatureEquivalence.get().wrap(method))) {
+      Wrapper<DexMethod> signature = MethodSignatureEquivalence.get().wrap(method);
+      if (!seenMethods.contains(signature)) {
         SingleResolutionResult resolution =
             appInfo.resolveMethodOn(superClass, method).asSingleResolution();
         assert resolution != null;
         assert resolution.getResolvedHolder().isProgramClass();
-        if (resolution != null && resolution.getResolvedHolder().isProgramClass()) {
-          markLiveOverrides(
-              instantiation, superClass, resolution.getResolutionPair().asProgramMethod());
+        if (resolution != null) {
+          if (!initialClass.isProgramClass()
+              || resolution
+                  .isAccessibleForVirtualDispatchFrom(initialClass.asProgramClass(), appInfo)
+                  .isTrue()) {
+            seenMethods.add(signature);
+          }
+          if (resolution.getResolvedHolder().isProgramClass()) {
+            markLiveOverrides(
+                instantiation, superClass, resolution.getResolutionPair().asProgramMethod());
+          }
         }
       }
     }
@@ -2370,6 +2384,8 @@
     // Add all dependent members to the workqueue.
     enqueueRootItems(rootSet.getDependentItems(field.getDefinition()));
 
+    checkMemberForSoftPinning(field);
+
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveField(field));
   }
@@ -2386,6 +2402,8 @@
     // Add all dependent members to the workqueue.
     enqueueRootItems(rootSet.getDependentItems(field.getDefinition()));
 
+    checkMemberForSoftPinning(field);
+
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveField(field));
   }
@@ -3400,12 +3418,26 @@
             enqueueRootItems(dependentItems);
           }
         });
+    consequentRootSet.dependentSoftPinned.forEach(
+        (reference, dependentItems) -> {
+          if (isLiveProgramReference(reference)) {
+            dependentItems.forEachReference(
+                item -> {
+                  if (isLiveProgramReference(item)) {
+                    keepInfo.joinInfo(item, appView, Joiner::pin);
+                  }
+                });
+          }
+        });
+
     // TODO(b/132600955): This modifies the root set. Should the consequent be persistent?
     rootSet.addConsequentRootSet(consequentRootSet, addNoShrinking);
     if (mode.isInitialTreeShaking()) {
       for (DexReference reference : consequentRootSet.noObfuscation) {
         keepInfo.evaluateRule(reference, appView, Joiner::disallowMinification);
       }
+      consequentRootSet.softPinned.forEachReference(
+          reference -> keepInfo.evaluateRule(reference, appView, Joiner::pin));
     }
     enqueueRootItems(consequentRootSet.noShrinking);
     // Check for compatibility rules indicating that the holder must be implicitly kept.
@@ -3419,6 +3451,18 @@
     }
   }
 
+  private boolean isLiveProgramReference(DexReference reference) {
+    if (reference.isDexType()) {
+      DexProgramClass clazz =
+          DexProgramClass.asProgramClassOrNull(definitionFor(reference.asDexType()));
+      return clazz != null && isTypeLive(clazz);
+    }
+    DexMember<?, ?> member = reference.asDexMember();
+    DexProgramClass holder = DexProgramClass.asProgramClassOrNull(definitionFor(member.holder));
+    ProgramMember<?, ?> programMember = member.lookupOnProgramClass(holder);
+    return programMember != null && isMemberLive(programMember.getDefinition());
+  }
+
   private ConsequentRootSet computeDelayedInterfaceMethodSyntheticBridges() {
     RootSetBuilder builder = new RootSetBuilder(appView, subtypingInfo);
     for (DelayedRootSetActionItem delayedRootSetActionItem : rootSet.delayedRootSetActionItems) {
@@ -3430,7 +3474,8 @@
     return builder.buildConsequentRootSet();
   }
 
-  private Map<DexMethod, ProgramMethod> syntheticInterfaceMethodBridges = new IdentityHashMap<>();
+  private final Map<DexMethod, ProgramMethod> syntheticInterfaceMethodBridges =
+      new LinkedHashMap<>();
 
   private void handleInterfaceMethodSyntheticBridgeAction(
       InterfaceMethodSyntheticBridgeAction action, RootSetBuilder builder) {
@@ -3608,10 +3653,28 @@
     // Add all dependent members to the workqueue.
     enqueueRootItems(rootSet.getDependentItems(definition));
 
+    checkMemberForSoftPinning(method);
+
     // Notify analyses.
     analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method));
   }
 
+  private void checkMemberForSoftPinning(ProgramMember<?, ?> member) {
+    DexMember<?, ?> reference = member.getDefinition().toReference();
+    Set<ProguardKeepRuleBase> softPinRules = rootSet.softPinned.getRulesForReference(reference);
+    if (softPinRules != null) {
+      assert softPinRules.stream().noneMatch(r -> r.getModifiers().allowsOptimization);
+      keepInfo.joinInfo(reference, appInfo, Joiner::pin);
+    }
+    // Identify dependent soft pinning.
+    MutableItemsWithRules items = rootSet.dependentSoftPinned.get(member.getHolderType());
+    if (items != null && items.containsReference(reference)) {
+      assert items.getRulesForReference(reference).stream()
+          .noneMatch(r -> r.getModifiers().allowsOptimization);
+      keepInfo.joinInfo(reference, appInfo, Joiner::pin);
+    }
+  }
+
   private void markReferencedTypesAsLive(ProgramMethod method) {
     markTypeAsLive(
         method.getHolderType(), clazz -> graphReporter.reportClassReferencedFrom(clazz, method));
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 c6a1725..2de5c70 100644
--- a/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
+++ b/src/main/java/com/android/tools/r8/shaking/LibraryMethodOverrideAnalysis.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -222,13 +223,13 @@
         }
       }
 
-      DexEncodedMethod singleTarget = invoke.lookupSingleTarget(appView, context);
+      DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
       if (singleTarget == null) {
         return false;
       }
 
       InstanceInitializerInfo initializerInfo =
-          singleTarget.getOptimizationInfo().getInstanceInitializerInfo();
+          singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo();
       return initializerInfo.receiverNeverEscapesOutsideConstructorChain();
     }
   }
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 f8b5987..a5ee8b7 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -5,8 +5,8 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
-import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.AssumeNoSideEffectsRuleForObjectMembersDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -67,6 +67,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.Stack;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -83,6 +84,7 @@
   private final DirectMappedDexApplication application;
   private final Iterable<? extends ProguardConfigurationRule> rules;
   private final MutableItemsWithRules noShrinking = new MutableItemsWithRules();
+  private final MutableItemsWithRules softPinned = new MutableItemsWithRules();
   private final Set<DexReference> noObfuscation = Sets.newIdentityHashSet();
   private final LinkedHashMap<DexReference, DexReference> reasonAsked = new LinkedHashMap<>();
   private final LinkedHashMap<DexReference, DexReference> checkDiscarded = new LinkedHashMap<>();
@@ -103,11 +105,13 @@
   private final Set<DexReference> neverPropagateValue = Sets.newIdentityHashSet();
   private final Map<DexReference, MutableItemsWithRules> dependentNoShrinking =
       new IdentityHashMap<>();
+  private final Map<DexReference, MutableItemsWithRules> dependentSoftPinned =
+      new IdentityHashMap<>();
   private final Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule =
       new IdentityHashMap<>();
   private final Map<DexReference, ProguardMemberRule> mayHaveSideEffects = new IdentityHashMap<>();
-  private final Map<DexReference, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
-  private final Map<DexReference, ProguardMemberRule> assumedValues = new IdentityHashMap<>();
+  private final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
+  private final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues = new IdentityHashMap<>();
   private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet();
   private final Queue<DelayedRootSetActionItem> delayedRootSetActionItems =
       new ConcurrentLinkedQueue<>();
@@ -116,8 +120,8 @@
   private final DexStringCache dexStringCache = new DexStringCache();
   private final Set<ProguardIfRule> ifRules = Sets.newIdentityHashSet();
 
-  private final Map<OriginWithPosition, List<DexMethod>> assumeNoSideEffectsWarnings =
-      new HashMap<>();
+  private final Map<OriginWithPosition, Set<DexMethod>> assumeNoSideEffectsWarnings =
+      new LinkedHashMap<>();
 
   public RootSetBuilder(
       AppView<? extends AppInfoWithClassHierarchy> appView,
@@ -334,6 +338,7 @@
     assert appView.options().isMinificationEnabled() || noObfuscation.isEmpty();
     return new RootSet(
         noShrinking,
+        softPinned,
         noObfuscation,
         ImmutableList.copyOf(reasonAsked.values()),
         ImmutableList.copyOf(checkDiscarded.values()),
@@ -356,6 +361,7 @@
         noSideEffects,
         assumedValues,
         dependentNoShrinking,
+        dependentSoftPinned,
         dependentKeepClassCompatRule,
         identifierNameStrings,
         ifRules,
@@ -382,7 +388,7 @@
       DexType type,
       DexMethod reference,
       Set<DexType> subTypes,
-      Map<DexReference, ProguardMemberRule> assumeRulePool) {
+      Map<DexMember<?, ?>, ProguardMemberRule> assumeRulePool) {
     ProguardMemberRule ruleToBePropagated = null;
     for (DexType subType : subTypes) {
       DexMethod referenceInSubType =
@@ -424,8 +430,10 @@
         neverInline,
         neverClassInline,
         noShrinking,
+        softPinned,
         noObfuscation,
         dependentNoShrinking,
+        dependentSoftPinned,
         dependentKeepClassCompatRule,
         Lists.newArrayList(delayedRootSetActionItems));
   }
@@ -1136,6 +1144,14 @@
           noShrinking.addReferenceWithRule(item.toReference(), keepRule);
         }
         context.markAsUsed();
+      } else if (!modifiers.allowsOptimization) {
+        if (precondition != null) {
+          dependentSoftPinned
+              .computeIfAbsent(precondition.toReference(), x -> new MutableItemsWithRules())
+              .addReferenceWithRule(item.toReference(), keepRule);
+        } else {
+          softPinned.addReferenceWithRule(item.toReference(), keepRule);
+        }
       }
       if (!modifiers.allowsOptimization) {
         // The -dontoptimize flag has only effect through the keep all rule, but we still
@@ -1155,15 +1171,25 @@
       mayHaveSideEffects.put(item.toReference(), rule);
       context.markAsUsed();
     } else if (context instanceof ProguardAssumeNoSideEffectRule) {
-      checkAssumeNoSideEffectsWarnings(item, (ProguardAssumeNoSideEffectRule) context, rule);
-      noSideEffects.put(item.toReference(), rule);
-      context.markAsUsed();
+      if (item.isDexEncodedMember()) {
+        DexEncodedMember<?, ?> member = item.asDexEncodedMember();
+        if (member.holder() == appView.dexItemFactory().objectType) {
+          assert member.isDexEncodedMethod();
+          reportAssumeNoSideEffectsWarningForJavaLangClassMethod(
+              member.asDexEncodedMethod(), (ProguardAssumeNoSideEffectRule) context);
+        } else {
+          noSideEffects.put(member.toReference(), rule);
+        }
+        context.markAsUsed();
+      }
     } else if (context instanceof ProguardWhyAreYouKeepingRule) {
       reasonAsked.computeIfAbsent(item.toReference(), i -> i);
       context.markAsUsed();
     } else if (context instanceof ProguardAssumeValuesRule) {
-      assumedValues.put(item.toReference(), rule);
-      context.markAsUsed();
+      if (item.isDexEncodedMember()) {
+        assumedValues.put(item.asDexEncodedMember().toReference(), rule);
+        context.markAsUsed();
+      }
     } else if (context instanceof ProguardCheckDiscardRule) {
       checkDiscarded.computeIfAbsent(item.toReference(), i -> i);
       context.markAsUsed();
@@ -1301,8 +1327,10 @@
     final Set<DexMethod> neverInline;
     final Set<DexType> neverClassInline;
     final MutableItemsWithRules noShrinking;
+    final MutableItemsWithRules softPinned;
     final Set<DexReference> noObfuscation;
     final Map<DexReference, MutableItemsWithRules> dependentNoShrinking;
+    final Map<DexReference, MutableItemsWithRules> dependentSoftPinned;
     final Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule;
     final List<DelayedRootSetActionItem> delayedRootSetActionItems;
 
@@ -1310,15 +1338,19 @@
         Set<DexMethod> neverInline,
         Set<DexType> neverClassInline,
         MutableItemsWithRules noShrinking,
+        MutableItemsWithRules softPinned,
         Set<DexReference> noObfuscation,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
+        Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
         List<DelayedRootSetActionItem> delayedRootSetActionItems) {
       this.neverInline = neverInline;
       this.neverClassInline = neverClassInline;
       this.noShrinking = noShrinking;
+      this.softPinned = softPinned;
       this.noObfuscation = noObfuscation;
       this.dependentNoShrinking = dependentNoShrinking;
+      this.dependentSoftPinned = dependentSoftPinned;
       this.dependentKeepClassCompatRule = dependentKeepClassCompatRule;
       this.delayedRootSetActionItems = delayedRootSetActionItems;
     }
@@ -1445,19 +1477,20 @@
       return reference.apply(this::containsClass, this::containsField, this::containsMethod);
     }
 
-    public abstract void forEachClass(Consumer<DexType> consumer);
+    public abstract void forEachClass(Consumer<? super DexType> consumer);
 
-    public abstract void forEachClass(BiConsumer<DexType, Set<ProguardKeepRuleBase>> consumer);
+    public abstract void forEachClass(
+        BiConsumer<? super DexType, Set<ProguardKeepRuleBase>> consumer);
 
     public abstract void forEachField(Consumer<? super DexField> consumer);
 
     public abstract void forEachField(
         BiConsumer<? super DexField, Set<ProguardKeepRuleBase>> consumer);
 
-    public abstract void forEachMember(Consumer<DexMember<?, ?>> consumer);
+    public abstract void forEachMember(Consumer<? super DexMember<?, ?>> consumer);
 
     public abstract void forEachMember(
-        BiConsumer<DexMember<?, ?>, Set<ProguardKeepRuleBase>> consumer);
+        BiConsumer<? super DexMember<?, ?>, Set<ProguardKeepRuleBase>> consumer);
 
     public abstract void forEachMethod(Consumer<? super DexMethod> consumer);
 
@@ -1554,13 +1587,18 @@
       return methodsWithRules.containsKey(method);
     }
 
+    public void forEachReference(Consumer<DexReference> consumer) {
+      forEachClass(consumer);
+      forEachMember(consumer);
+    }
+
     @Override
-    public void forEachClass(Consumer<DexType> consumer) {
+    public void forEachClass(Consumer<? super DexType> consumer) {
       classesWithRules.keySet().forEach(consumer);
     }
 
     @Override
-    public void forEachClass(BiConsumer<DexType, Set<ProguardKeepRuleBase>> consumer) {
+    public void forEachClass(BiConsumer<? super DexType, Set<ProguardKeepRuleBase>> consumer) {
       classesWithRules.forEach(consumer);
     }
 
@@ -1575,13 +1613,14 @@
     }
 
     @Override
-    public void forEachMember(Consumer<DexMember<?, ?>> consumer) {
+    public void forEachMember(Consumer<? super DexMember<?, ?>> consumer) {
       forEachField(consumer);
       forEachMethod(consumer);
     }
 
     @Override
-    public void forEachMember(BiConsumer<DexMember<?, ?>, Set<ProguardKeepRuleBase>> consumer) {
+    public void forEachMember(
+        BiConsumer<? super DexMember<?, ?>, Set<ProguardKeepRuleBase>> consumer) {
       forEachField(consumer);
       forEachMethod(consumer);
     }
@@ -1655,18 +1694,13 @@
     }
   }
 
-  private void checkAssumeNoSideEffectsWarnings(
-      DexDefinition item, ProguardAssumeNoSideEffectRule context, ProguardMemberRule rule) {
-    if (rule.getRuleType() == ProguardMemberType.METHOD && rule.isSpecific()) {
-      return;
-    }
-    if (item.isDexEncodedMethod()) {
-      DexEncodedMethod method = item.asDexEncodedMethod();
-      if (method.holder() == options.itemFactory.objectType) {
-        OriginWithPosition key = new OriginWithPosition(context.getOrigin(), context.getPosition());
-        assumeNoSideEffectsWarnings.computeIfAbsent(key, k -> new ArrayList<>()).add(method.method);
-      }
-    }
+  private void reportAssumeNoSideEffectsWarningForJavaLangClassMethod(
+      DexEncodedMethod method, ProguardAssumeNoSideEffectRule context) {
+    assert method.getHolderType() == options.dexItemFactory().objectType;
+    OriginWithPosition key = new OriginWithPosition(context.getOrigin(), context.getPosition());
+    assumeNoSideEffectsWarnings
+        .computeIfAbsent(key, ignore -> new TreeSet<>(DexMethod::slowCompareTo))
+        .add(method.getReference());
   }
 
   private boolean isWaitOrNotifyMethod(DexMethod method) {
@@ -1680,50 +1714,27 @@
         options.getProguardConfiguration() != null
             ? options.getProguardConfiguration().getDontWarnPatterns()
             : ProguardClassFilter.empty();
+    if (dontWarnPatterns.matches(options.itemFactory.objectType)) {
+      // Don't report any warnings since we don't apply -assumenosideeffects rules to notify() or
+      // wait() anyway.
+      return;
+    }
 
     assumeNoSideEffectsWarnings.forEach(
         (originWithPosition, methods) -> {
-          boolean waitOrNotifyMethods = methods.stream().anyMatch(this::isWaitOrNotifyMethod);
-          boolean dontWarnObject = dontWarnPatterns.matches(options.itemFactory.objectType);
-          StringBuilder message = new StringBuilder();
-          message.append(
-              "The -assumenosideeffects rule matches methods on `java.lang.Object` with wildcards");
-          message.append(" including the method(s) ");
-          for (int i = 0; i < methods.size(); i++) {
-            if (i > 0) {
-              message.append(i < methods.size() - 1 ? ", " : " and ");
-            }
-            message.append("`");
-            message.append(methods.get(i).toSourceStringWithoutHolder());
-            message.append("`.");
+          boolean matchesWaitOrNotifyMethods =
+              methods.stream().anyMatch(this::isWaitOrNotifyMethod);
+          if (!matchesWaitOrNotifyMethods) {
+            // We model the remaining methods on java.lang.Object, and thus there should be no need
+            // to warn in this case.
+            return;
           }
-          if (waitOrNotifyMethods) {
-            message.append(" This will most likely cause problems.");
-          } else {
-            message.append(" This is most likely not intended.");
-          }
-          if (waitOrNotifyMethods && !dontWarnObject) {
-            message.append(" Specify the methods more precisely.");
-          } else {
-            message.append(" Consider specifying the methods more precisely.");
-          }
-          Diagnostic diagnostic =
-              new StringDiagnostic(
-                  message.toString(),
-                  originWithPosition.getOrigin(),
-                  originWithPosition.getPosition());
-          if (waitOrNotifyMethods) {
-            if (!dontWarnObject) {
-              options.reporter.error(diagnostic);
-            } else {
-              options.reporter.warning(diagnostic);
-            }
-
-          } else {
-            if (!dontWarnObject) {
-              options.reporter.warning(diagnostic);
-            }
-          }
+          options.reporter.warning(
+              new AssumeNoSideEffectsRuleForObjectMembersDiagnostic.Builder()
+                  .addMatchedMethods(methods)
+                  .setOrigin(originWithPosition.getOrigin())
+                  .setPosition(originWithPosition.getPosition())
+                  .build());
         });
   }
 
@@ -1745,13 +1756,14 @@
     public final Set<DexType> noStaticClassMerging;
     public final Set<DexReference> neverPropagateValue;
     public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
-    public final Map<DexReference, ProguardMemberRule> noSideEffects;
-    public final Map<DexReference, ProguardMemberRule> assumedValues;
+    public final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects;
+    public final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues;
     public final Set<DexReference> identifierNameStrings;
     public final Set<ProguardIfRule> ifRules;
 
     private RootSet(
         MutableItemsWithRules noShrinking,
+        MutableItemsWithRules softPinned,
         Set<DexReference> noObfuscation,
         ImmutableList<DexReference> reasonAsked,
         ImmutableList<DexReference> checkDiscarded,
@@ -1771,9 +1783,10 @@
         Set<DexType> noStaticClassMerging,
         Set<DexReference> neverPropagateValue,
         Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-        Map<DexReference, ProguardMemberRule> noSideEffects,
-        Map<DexReference, ProguardMemberRule> assumedValues,
+        Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+        Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
+        Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
         Set<DexReference> identifierNameStrings,
         Set<ProguardIfRule> ifRules,
@@ -1782,8 +1795,10 @@
           neverInline,
           neverClassInline,
           noShrinking,
+          softPinned,
           noObfuscation,
           dependentNoShrinking,
+          dependentSoftPinned,
           dependentKeepClassCompatRule,
           delayedRootSetActionItems);
       this.reasonAsked = reasonAsked;
@@ -1831,7 +1846,8 @@
       if (addNoShrinking) {
         noShrinking.addAll(consequentRootSet.noShrinking);
       }
-      addDependentItems(consequentRootSet.dependentNoShrinking);
+      addDependentItems(consequentRootSet.dependentNoShrinking, dependentNoShrinking);
+      addDependentItems(consequentRootSet.dependentSoftPinned, dependentSoftPinned);
       consequentRootSet.dependentKeepClassCompatRule.forEach(
           (type, rules) ->
               dependentKeepClassCompatRule.computeIfAbsent(
@@ -1840,10 +1856,12 @@
     }
 
     // Add dependent items that depend on -if rules.
-    private void addDependentItems(Map<DexReference, ? extends ItemsWithRules> dependentItems) {
-      dependentItems.forEach(
+    private static void addDependentItems(
+        Map<DexReference, ? extends ItemsWithRules> dependentItemsToAdd,
+        Map<DexReference, MutableItemsWithRules> dependentItemsToAddTo) {
+      dependentItemsToAdd.forEach(
           (reference, dependence) ->
-              dependentNoShrinking
+              dependentItemsToAddTo
                   .computeIfAbsent(reference, x -> new MutableItemsWithRules())
                   .putAll(dependence));
     }
@@ -1855,11 +1873,15 @@
       if (noObfuscation.contains(original)) {
         noObfuscation.add(rewritten);
       }
-      if (noSideEffects.containsKey(original)) {
-        noSideEffects.put(rewritten, noSideEffects.get(original));
-      }
-      if (assumedValues.containsKey(original)) {
-        assumedValues.put(rewritten, assumedValues.get(original));
+      if (original.isDexMember()) {
+        assert rewritten.isDexMember();
+        DexMember<?, ?> originalMember = original.asDexMember();
+        if (noSideEffects.containsKey(originalMember)) {
+          noSideEffects.put(rewritten.asDexMember(), noSideEffects.get(originalMember));
+        }
+        if (assumedValues.containsKey(originalMember)) {
+          assumedValues.put(rewritten.asDexMember(), assumedValues.get(originalMember));
+        }
       }
     }
 
@@ -2075,16 +2097,20 @@
         Set<DexMethod> neverInline,
         Set<DexType> neverClassInline,
         MutableItemsWithRules noShrinking,
+        MutableItemsWithRules softPinned,
         Set<DexReference> noObfuscation,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
+        Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
         List<DelayedRootSetActionItem> delayedRootSetActionItems) {
       super(
           neverInline,
           neverClassInline,
           noShrinking,
+          softPinned,
           noObfuscation,
           dependentNoShrinking,
+          dependentSoftPinned,
           dependentKeepClassCompatRule,
           delayedRootSetActionItems);
     }
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 6100000..de95f92 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -241,8 +241,8 @@
     initializeMergeCandidates(classes);
   }
 
-  public VerticallyMergedClasses getMergedClasses() {
-    return new VerticallyMergedClasses(mergedClasses);
+  private VerticallyMergedClasses getMergedClasses() {
+    return new VerticallyMergedClasses(mergedClasses, mergedClassesInverse);
   }
 
   private void initializeMergeCandidates(Iterable<DexProgramClass> classes) {
@@ -1484,7 +1484,7 @@
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         synthesizedBridge.updateMethodSignatures(this::fixupMethod);
       }
-      VerticalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
+      VerticalClassMergerGraphLens lens = lensBuilder.build(appView, getMergedClasses());
       if (lens != null) {
         new AnnotationFixer(lens).run(appView.appInfo().classes());
       }
@@ -1728,6 +1728,11 @@
     }
 
     @Override
+    public Iterable<DexType> getOriginalTypes(DexType type) {
+      throw new Unreachable();
+    }
+
+    @Override
     public DexField getOriginalFieldSignature(DexField field) {
       throw new Unreachable();
     }
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 a95cfb4..a5481fd 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -14,10 +14,14 @@
 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.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -55,6 +59,7 @@
 
   private final AppView<?> appView;
 
+  private VerticallyMergedClasses mergedClasses;
   private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
       contextualVirtualToDirectMethodMaps;
   private Set<DexMethod> mergedMethods;
@@ -62,7 +67,7 @@
 
   private VerticalClassMergerGraphLens(
       AppView<?> appView,
-      Map<DexType, DexType> typeMap,
+      VerticallyMergedClasses mergedClasses,
       Map<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
       Set<DexMethod> mergedMethods,
@@ -73,7 +78,7 @@
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
       GraphLens previousLens) {
     super(
-        typeMap,
+        mergedClasses.getForwardMap(),
         methodMap,
         fieldMap,
         originalFieldSignatures,
@@ -81,14 +86,24 @@
         previousLens,
         appView.dexItemFactory());
     this.appView = appView;
+    this.mergedClasses = mergedClasses;
     this.contextualVirtualToDirectMethodMaps = contextualVirtualToDirectMethodMaps;
     this.mergedMethods = mergedMethods;
     this.originalMethodSignaturesForBridges = originalMethodSignaturesForBridges;
   }
 
+  public VerticallyMergedClasses getMergedClasses() {
+    return mergedClasses;
+  }
+
   @Override
-  public DexType getOriginalType(DexType type) {
-    return getPrevious().getOriginalType(type);
+  protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
+    Collection<DexType> originalTypes = mergedClasses.getSourcesFor(previous);
+    Iterable<DexType> currentType = IterableUtils.singleton(previous);
+    if (originalTypes == null) {
+      return currentType;
+    }
+    return Iterables.concat(currentType, originalTypes);
   }
 
   @Override
@@ -219,8 +234,8 @@
     }
 
     public VerticalClassMergerGraphLens build(
-        AppView<?> appView, Map<DexType, DexType> mergedClasses) {
-      if (mergedClasses.isEmpty()) {
+        AppView<?> appView, VerticallyMergedClasses mergedClasses) {
+      if (mergedClasses.getForwardMap().isEmpty()) {
         return null;
       }
       BiMap<DexField, DexField> originalFieldSignatures = fieldMap.inverse();
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 f4c003e..ea902d5 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -166,6 +166,7 @@
     itemFactory = proguardConfiguration.getDexItemFactory();
     enableTreeShaking = proguardConfiguration.isShrinking();
     enableMinification = proguardConfiguration.isObfuscating();
+    // TODO(b/171457102): Avoid the need for this.
     // -dontoptimize disables optimizations by flipping related flags.
     if (!proguardConfiguration.isOptimizing()) {
       disableAllOptimizations();
@@ -234,8 +235,7 @@
   public boolean enableFieldBitAccessAnalysis =
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
   public boolean enableStaticClassMerging = true;
-  public boolean enableHorizontalClassMerging =
-      System.getProperty("com.android.tools.r8.horizontalClassMerging") != null;
+  public boolean enableHorizontalClassMerging = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableArgumentRemoval = true;
   public boolean enableUnusedInterfaceRemoval = true;
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 15a8198..57cf52b 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -4,9 +4,12 @@
 
 package com.android.tools.r8.utils;
 
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 public class IterableUtils {
@@ -53,4 +56,25 @@
   public static <T> boolean isEmpty(Iterable<T> iterable) {
     return !iterable.iterator().hasNext();
   }
+
+  public static <T> Iterable<T> singleton(T element) {
+    return () -> Iterators.singletonIterator(element);
+  }
+
+  public static <T> Iterable<T> prependSingleton(T t, Iterable<T> iterable) {
+    return Iterables.concat(singleton(t), iterable);
+  }
+
+  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> emptyIf(Iterable<T> iterable, boolean condition) {
+    if (condition) {
+      return Collections.emptySet();
+    } else {
+      return iterable;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index f941485..51821c0 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -7,6 +7,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -63,4 +64,12 @@
     }
     return true;
   }
+
+  public static <T, R> R fold(Collection<T> items, R identity, BiFunction<R, T, R> acc) {
+    R result = identity;
+    for (T item : items) {
+      result = acc.apply(result, item);
+    }
+    return result;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
new file mode 100644
index 0000000..7f48f2a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.TypeReference;
+import java.util.Iterator;
+
+public class MethodReferenceUtils {
+
+  public static String toSourceStringWithoutHolderAndReturnType(MethodReference methodReference) {
+    return toSourceString(methodReference, false, false);
+  }
+
+  public static String toSourceString(
+      MethodReference methodReference, boolean includeHolder, boolean includeReturnType) {
+    StringBuilder builder = new StringBuilder();
+    if (includeReturnType) {
+      builder.append(methodReference.getReturnType().getTypeName()).append(" ");
+    }
+    if (includeHolder) {
+      builder.append(methodReference.getHolderClass().getTypeName()).append(".");
+    }
+    builder.append(methodReference.getMethodName()).append("(");
+    Iterator<TypeReference> formalTypesIterator = methodReference.getFormalTypes().iterator();
+    if (formalTypesIterator.hasNext()) {
+      builder.append(formalTypesIterator.next().getTypeName());
+      while (formalTypesIterator.hasNext()) {
+        builder.append(", ").append(formalTypesIterator.next().getTypeName());
+      }
+    }
+    return builder.append(")").toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
index 3c3d37c..7410339 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
@@ -45,6 +45,10 @@
     return inverse.getOrDefault(value, Collections.emptySet());
   }
 
+  public Set<K> getKeysOrNull(V value) {
+    return inverse.get(value);
+  }
+
   public boolean isEmpty() {
     return backing.isEmpty();
   }
diff --git a/src/test/java/com/android/tools/r8/ProguardVersion.java b/src/test/java/com/android/tools/r8/ProguardVersion.java
index 8c9050a..20560fc 100644
--- a/src/test/java/com/android/tools/r8/ProguardVersion.java
+++ b/src/test/java/com/android/tools/r8/ProguardVersion.java
@@ -20,6 +20,10 @@
     this.version = version;
   }
 
+  public static ProguardVersion getLatest() {
+    return V7_0_0;
+  }
+
   public Path getProguardScript() {
     Path scriptDirectory = Paths.get(ToolHelper.THIRD_PARTY_DIR).resolve("proguard");
     if (this == V7_0_0) {
@@ -33,8 +37,12 @@
     return scriptDirectory.resolve("bin/proguard.sh");
   }
 
+  public String getVersion() {
+    return version;
+  }
+
   @Override
   public String toString() {
-    return "Proguard " + version.toString();
+    return "Proguard " + version;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index c03cd49..0b9c896 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -31,6 +31,7 @@
 
 @RunWith(VmTestRunner.class)
 public class R8RunExamplesAndroidOTest extends RunExamplesAndroidOTest<R8Command.Builder> {
+
   private static final ArrayList<String> PROGUARD_OPTIONS = Lists.newArrayList(
       "-keepclasseswithmembers public class * {",
       "    public static void main(java.lang.String[]);",
@@ -161,7 +162,6 @@
   @Override
   @Test
   public void lambdaDesugaringNPlus() throws Throwable {
-    expectThrowsWithHorizontalClassMerging();
     test("lambdadesugaringnplus", "lambdadesugaringnplus", "LambdasWithStaticAndDefaultMethods")
         .withMinApiLevel(ToolHelper.getMinApiLevelForDexVmNoHigherThan(AndroidApiLevel.K))
         .withInterfaceMethodDesugaring(OffOrAuto.Auto)
@@ -179,7 +179,7 @@
         .withBuilderTransformation(ToolHelper::allowTestProguardOptions)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS_N_PLUS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 2, "lambdadesugaringnplus"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 3, "lambdadesugaringnplus"))
         .run();
   }
 
@@ -207,7 +207,7 @@
         .run();
   }
 
-  private void checkLambdaCount(CodeInspector inspector, int expectedCount, String prefix) {
+  private void checkLambdaCount(CodeInspector inspector, int maxExpectedCount, String prefix) {
     int count = 0;
     for (FoundClassSubject clazz : inspector.allClasses()) {
       if (clazz.isSynthesizedJavaLambdaClass() &&
@@ -215,7 +215,7 @@
         count++;
       }
     }
-    assertEquals(expectedCount, count);
+    assertEquals(maxExpectedCount, count);
   }
 
   private void checkTestMultipleInterfacesCheckCastCount(
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 27c1570..f18a09f 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1713,32 +1713,4 @@
         .compile()
         .writeToZip();
   }
-
-  public static <E extends Throwable> void assertThrowsWithHorizontalClassMerging(
-      ThrowingAction<E> action) throws E {
-    try {
-      action.execute();
-      if (isHorizontalClassMergingEnabled()) {
-        fail();
-      }
-    } catch (Throwable throwable) {
-      if (!isHorizontalClassMergingEnabled()) {
-        throw throwable;
-      }
-    }
-  }
-
-  public void expectThrowsWithHorizontalClassMerging() {
-    expectThrowsWithHorizontalClassMergingIf(true);
-  }
-
-  public void expectThrowsWithHorizontalClassMergingIf(boolean condition) {
-    if (isHorizontalClassMergingEnabled() && condition) {
-      thrown.expect(Throwable.class);
-    }
-  }
-
-  private static boolean isHorizontalClassMergingEnabled() {
-    return System.getProperty("com.android.tools.r8.horizontalClassMerging") != null;
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 9676011..3ad6107 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -189,6 +189,8 @@
       Paths.get(LIBS_DIR, "library_desugar_conversions.zip");
   public static final Path DESUGAR_LIB_JSON_FOR_TESTING =
       Paths.get("src/library_desugar/desugar_jdk_libs.json");
+  public static final Path DESUGAR_LIB_JSON_FOR_TESTING_ALTERNATIVE_3 =
+      Paths.get("src/library_desugar/desugar_jdk_libs_alternative_3.json");
 
   public static boolean isLocalDevelopment() {
     return System.getProperty("local_development", "0").equals("1");
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 bf103e4..11d410f 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/NonConstructorRelaxationTest.java
@@ -47,7 +47,6 @@
 
   @Test
   public void testStaticMethodRelaxation() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     String expectedOutput =
         StringUtils.lines(
             "A::baz()",
@@ -80,6 +79,7 @@
         testForR8(parameters.getBackend())
             .addProgramFiles(ToolHelper.getClassFilesForTestPackage(mainClass.getPackage()))
             .enableInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableMemberValuePropagationAnnotations()
             .addKeepMainRule(mainClass)
             .addOptionsModification(o -> o.enableArgumentRemoval = enableArgumentRemoval)
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
index 01ec674..a6c40ff 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/privatestatic/BB.java
@@ -6,7 +6,9 @@
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class BB extends A {
   @NeverInline
   @NeverPropagateValue
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
index 2665473..dbdb906 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/PositiveBridgeHoistingTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -34,7 +35,6 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class, A.class, B3.class, B4.class)
         .addProgramClassFileData(
@@ -53,6 +53,7 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
@@ -134,6 +135,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class B2 extends A {
 
     // By hoisting B1.superBridge() to A this method bridge redundant.
@@ -160,11 +162,13 @@
   // instruction targeting B3.virtualBridge() that fails with a NoSuchMethodError in the Enqueuer,
   // but this should never be the case in practice.
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class B3 extends A {}
 
   // The fact that this class declares superBridge() and virtualBridge() should not prevent
   // us from hoisting other bridges to A.
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class B4 extends A {
 
     @NeverInline
@@ -181,6 +185,7 @@
   // This class declares the same bridges, but with different (bridge) behavior. They are candidates
   // for hoisting, but will not be hoisted because it is better to hoist the bridges declared on B1.
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class B5 extends A {
 
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java
new file mode 100644
index 0000000..2c4ac9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.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.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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.B;
+import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.Main;
+import com.android.tools.r8.classmerging.horizontal.ServiceLoaderTest.A;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DataResourceConsumerForTesting;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class AdaptResourceFileContentsTest extends HorizontalClassMergingTestBase {
+  public AdaptResourceFileContentsTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting();
+    CodeInspector codeInspector =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(getClass())
+            .addKeepMainRule(Main.class)
+            .addOptionsModification(
+                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            .addOptionsModification(options -> options.dataResourceConsumer = dataResourceConsumer)
+            .enableNeverClassInliningAnnotations()
+            .addDataEntryResources(
+                DataEntryResource.fromString(
+                    "foo.txt", Origin.unknown(), A.class.getTypeName(), B.class.getTypeName()))
+            .addKeepRules("-adaptresourcefilecontents foo.txt")
+            .setMinApi(parameters.getApiLevel())
+            .addHorizontallyMergedClassesInspectorIf(
+                enableHorizontalClassMerging,
+                inspector -> inspector.assertMergedInto(B.class, A.class))
+            .compile()
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutputLines("a", "b")
+            .inspector();
+
+    ClassSubject aClassSubject = codeInspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    ClassSubject bClassSubject = codeInspector.clazz(B.class);
+    assertThat(bClassSubject, notIf(isPresent(), enableHorizontalClassMerging));
+
+    // Check that the class name has been rewritten.
+    String newClassBName =
+        (enableHorizontalClassMerging ? aClassSubject : bClassSubject).getFinalName();
+    assertEquals(
+        dataResourceConsumer.get("foo.txt"),
+        ImmutableList.of(aClassSubject.getFinalName(), newClassBName));
+  }
+
+  @NeverClassInline
+  public static class A {
+    public A() {
+      System.out.println("a");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public B() {
+      System.out.println("b");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A();
+      new B();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java
new file mode 100644
index 0000000..ea50082
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java
@@ -0,0 +1,107 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DataResourceConsumerForTesting;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class AdaptVerticallyMergedResourceFileContentsTest extends HorizontalClassMergingTestBase {
+  public AdaptVerticallyMergedResourceFileContentsTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting();
+    CodeInspector codeInspector =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(getClass())
+            .addKeepMainRule(Main.class)
+            .addOptionsModification(
+                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            .addOptionsModification(options -> options.dataResourceConsumer = dataResourceConsumer)
+            .enableNeverClassInliningAnnotations()
+            .addDataEntryResources(
+                DataEntryResource.fromString(
+                    "foo.txt",
+                    Origin.unknown(),
+                    Parent.class.getTypeName(),
+                    A.class.getTypeName(),
+                    B.class.getTypeName()))
+            .addKeepRules("-adaptresourcefilecontents foo.txt")
+            .setMinApi(parameters.getApiLevel())
+            .addHorizontallyMergedClassesInspectorIf(
+                enableHorizontalClassMerging,
+                inspector -> inspector.assertMergedInto(B.class, A.class))
+            .compile()
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutputLines("a", "foo parent", "b")
+            .inspector();
+
+    assertThat(codeInspector.clazz(Parent.class), not(isPresent()));
+
+    ClassSubject aClassSubject = codeInspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    ClassSubject bClassSubject = codeInspector.clazz(B.class);
+    assertThat(bClassSubject, notIf(isPresent(), enableHorizontalClassMerging));
+
+    // Check that the class name has been rewritten.
+    String newClassName =
+        (enableHorizontalClassMerging ? aClassSubject : bClassSubject).getFinalName();
+    assertEquals(
+        dataResourceConsumer.get("foo.txt"),
+        ImmutableList.of(aClassSubject.getFinalName(), aClassSubject.getFinalName(), newClassName));
+  }
+
+  @NeverClassInline
+  public static class Parent {
+    @NeverInline
+    public void foo() {
+      System.out.println("foo parent");
+    }
+  }
+
+  @NeverClassInline
+  public static class A extends Parent {
+    public A() {
+      System.out.println("a");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public B() {
+      System.out.println("b");
+    }
+  }
+
+  public static class Main {
+    @NeverInline
+    public static void parent(Parent p) {
+      p.foo();
+    }
+
+    public static void main(String[] args) {
+      parent(new A());
+      new B();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
index 7dae8b6..565c1bd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
@@ -44,16 +44,20 @@
 
               ClassSubject bClassSubject = codeInspector.clazz(B.class);
               assertThat(bClassSubject, isPresent());
-              methodSubject = bClassSubject.method("void", "foo");
-              assertThat(methodSubject, isPackagePrivate());
+              if (enableHorizontalClassMerging) {
+                methodSubject = bClassSubject.method("void", "foo$bridge");
+                assertThat(methodSubject, isPackagePrivate());
+              }
 
               assertThat(
                   codeInspector.clazz(C.class), notIf(isPresent(), enableHorizontalClassMerging));
 
               ClassSubject dClassSubject = codeInspector.clazz(D.class);
               assertThat(dClassSubject, isPresent());
-              methodSubject = dClassSubject.method("void", "foo");
-              assertThat(methodSubject, isPublic());
+              if (enableHorizontalClassMerging) {
+                methodSubject = dClassSubject.method("void", "foo$bridge");
+                assertThat(methodSubject, isPublic());
+              }
 
               ClassSubject eClassSubject = codeInspector.clazz(E.class);
               assertThat(eClassSubject, notIf(isPresent(), enableHorizontalClassMerging));
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
index 9783627..185e090 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
@@ -60,7 +60,7 @@
                 assertThat(
                     otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject printSubject = aClassSubject.method("void", "print");
+                MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
                 assertThat(printSubject, isPresent());
                 assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
index 109b0bc..a30149f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
@@ -67,7 +67,7 @@
                 assertThat(
                     otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject printSubject = aClassSubject.method("void", "print");
+                MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
                 assertThat(printSubject, isPresent());
                 assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
index fea20ed..f6def29 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
@@ -60,7 +60,7 @@
                 assertThat(
                     otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject printSubject = aClassSubject.method("void", "print");
+                MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
                 assertThat(printSubject, isPresent());
                 assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
new file mode 100644
index 0000000..a27fdbd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+
+public class InstantiatedAndUninstantiatedClassMergingTest extends HorizontalClassMergingTestBase {
+
+  public InstantiatedAndUninstantiatedClassMergingTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    test(testForR8(parameters.getBackend()));
+  }
+
+  @Test
+  public void testR8Compat() throws Exception {
+    test(testForR8Compat(parameters.getBackend()));
+  }
+
+  private <T extends R8TestBuilder<T>> void test(T testBuilder) throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(Instantiated.class), isPresent());
+              assertThat(inspector.clazz(Uninstantiated.class), isPresent());
+            })
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Instantiated", "Uninstantiated");
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new Instantiated();
+      Uninstantiated.method();
+    }
+  }
+
+  @NeverClassInline
+  public static final class Instantiated {
+
+    Instantiated() {
+      System.out.println("Instantiated");
+    }
+  }
+
+  public static final class Uninstantiated {
+
+    @NeverInline
+    static void method() {
+      System.out.println("Uninstantiated");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
index 605ff5e..2256e5d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
@@ -57,7 +57,7 @@
                 assertThat(
                     otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject printSubject = aClassSubject.method("void", "print");
+                MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
                 assertThat(printSubject, isPresent());
                 assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedSuperMethodIsDefaultMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedSuperMethodIsDefaultMethodTest.java
index 2fa8996..483e702 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedSuperMethodIsDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedSuperMethodIsDefaultMethodTest.java
@@ -54,6 +54,7 @@
     }
   }
 
+  @NoVerticalClassMerging
   public abstract static class A implements I {}
 
   @NeverClassInline
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java
new file mode 100644
index 0000000..00d6e59
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+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.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MergedVirtualMethodStackTraceTest extends HorizontalClassMergingTestBase {
+  public MergedVirtualMethodStackTraceTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  public StackTrace expectedStackTrace;
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(CfRuntime.getSystemRuntime(), Program.Main.class)
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(Program.class)
+        .addKeepMainRule(Program.Main.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .addDontWarn(C.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector -> inspector.assertMergedInto(Program.B.class, Program.A.class))
+        .run(parameters.getRuntime(), Program.Main.class)
+        .inspectStackTrace(
+            (stackTrace, codeInspector) -> {
+              assertThat(codeInspector.clazz(Program.A.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(Program.B.class),
+                  notIf(isPresent(), enableHorizontalClassMerging));
+              if (enableHorizontalClassMerging) {
+                StackTrace expectedStackTraceWithMergedMethod =
+                    StackTrace.builder()
+                        .add(expectedStackTrace)
+
+                        .add(
+                            1,
+                            StackTraceLine.builder()
+                                .setClassName(Program.A.class.getTypeName())
+                                .setMethodName("foo$bridge")
+                                .setFileName("Program.java")
+                                .setFileName(getClass().getSimpleName() + ".java")
+                                .setLineNumber(stackTrace.get(1).lineNumber)
+                                .build())
+                        .build();
+                assertThat(stackTrace, isSame(expectedStackTraceWithMergedMethod));
+              }
+            });
+  }
+
+  public static class C {
+    public static void foo() {
+      System.out.println("foo c");
+    }
+  }
+
+  public static class Program {
+    @NeverClassInline
+    public static class A {
+      @NeverInline
+      public void foo() {
+        System.out.println("foo a");
+        try {
+          // Undefined reference, prevents inlining.
+          C.foo();
+        } catch (NoClassDefFoundError e) {
+        }
+      }
+    }
+
+    @NeverClassInline
+    public static class B {
+      @NeverInline
+      public void foo() {
+        try {
+          // Undefined reference, prevents inlining.
+          C.foo();
+        } catch (NoClassDefFoundError e) {
+        }
+        throw new RuntimeException();
+      }
+    }
+
+    public static class Main {
+      public static void main(String[] args) {
+        new A().foo();
+        new B().foo();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/compatproguard/CompatKeepClassMemberNamesTestRunner.java b/src/test/java/com/android/tools/r8/compatproguard/CompatKeepClassMemberNamesTestRunner.java
index 412311e..de0e481 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/CompatKeepClassMemberNamesTestRunner.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/CompatKeepClassMemberNamesTestRunner.java
@@ -12,6 +12,7 @@
 
 import com.android.tools.r8.BaseCompilerCommand;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.TestParameters;
@@ -35,6 +36,8 @@
 @RunWith(Parameterized.class)
 public class CompatKeepClassMemberNamesTestRunner extends TestBase {
 
+  private static final ProguardVersion PG = ProguardVersion.getLatest();
+
   private static Class<?> MAIN_CLASS = CompatKeepClassMemberNamesTest.class;
   private static Class<?> BAR_CLASS = CompatKeepClassMemberNamesTest.Bar.class;
   private static Collection<Class<?>> CLASSES =
@@ -114,7 +117,7 @@
 
   @Test
   public void testWithoutRulesPG() throws Exception {
-    testWithoutRules(testForProguard());
+    testWithoutRules(testForProguard(PG));
   }
 
   @Test
@@ -157,7 +160,7 @@
 
   @Test
   public void testWithMembersRulePG() throws Exception {
-    assertMembersRuleCompatResult(buildWithMembersRule(testForProguard()).compile());
+    assertMembersRuleCompatResult(buildWithMembersRule(testForProguard(PG)).compile());
   }
 
   @Test
@@ -209,7 +212,7 @@
 
   @Test
   public void testWithNonStaticMembersRulePG() throws Exception {
-    assertBarIsAbsent(buildWithNonStaticMembersRule(testForProguard()).compile());
+    assertBarIsAbsent(buildWithNonStaticMembersRule(testForProguard(PG)).compile());
   }
 
   @Test
@@ -268,7 +271,7 @@
   @Test
   public void testWithMembersRuleEnableMinificationPG() throws Exception {
     assertMembersRuleEnableMinificationCompatResult(
-        buildWithMembersRuleEnableMinification(testForProguard()).compile());
+        buildWithMembersRuleEnableMinification(testForProguard(PG)).compile());
   }
 
   @Test
@@ -313,7 +316,7 @@
 
   @Test
   public void testWithMembersStarRulePG() throws Exception {
-    testWithMembersStarRule(testForProguard());
+    testWithMembersStarRule(testForProguard(PG));
   }
 
   @Test
@@ -362,7 +365,7 @@
 
   @Test
   public void testWithMemberNamesRulePG() throws Exception {
-    assertMemberNamesRuleCompatResult(buildWithMemberNamesRule(testForProguard()).compile());
+    assertMemberNamesRuleCompatResult(buildWithMemberNamesRule(testForProguard(PG)).compile());
   }
 
   @Test
@@ -375,7 +378,8 @@
 
   @Test
   public void testWithMemberNamesRuleFullR8() throws Exception {
-    assertBarIsAbsent(buildWithMemberNamesRule(testForR8Compat(parameters.getBackend())).compile());
+    assertMemberNamesRuleCompatResult(
+        buildWithMemberNamesRule(testForR8(parameters.getBackend())).compile());
   }
 
   // Tests for "-keepclassmembernames" and *no* minification.
@@ -419,7 +423,7 @@
   @Test
   public void testWithMemberNamesRuleEnableMinificationPG() throws Exception {
     assertMemberNamesRuleEnableMinificationCompatResult(
-        buildWithMemberNamesRuleEnableMinification(testForProguard()).compile());
+        buildWithMemberNamesRuleEnableMinification(testForProguard(PG)).compile());
   }
 
   @Test
@@ -432,7 +436,7 @@
 
   @Test
   public void testWithMemberNamesRuleEnableMinificationFullR8() throws Exception {
-    assertBarIsAbsent(
+    assertMemberNamesRuleEnableMinificationCompatResult(
         buildWithMemberNamesRuleEnableMinification(testForR8(parameters.getBackend())).compile());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 44bcc80..13d59dc 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -2108,6 +2108,14 @@
           }
           return false;
         }
+        if (isInLambdaClass(mirror, location)) {
+          // Lambda classes must be skipped since they are only wrappers around lambda code.
+          if (DEBUG_TESTS) {
+            System.out.println("Skipping lambda class wrapper method");
+          }
+          wrapper.enqueueCommandFirst(stepCommand);
+          return true;
+        }
         if (isSyntheticMethod(mirror, location)) {
           if (DEBUG_TESTS) {
             System.out.println("Skipping synthetic method");
@@ -2152,6 +2160,11 @@
         return false;
       }
 
+      private static boolean isInLambdaClass(VmMirror mirror, Location location) {
+        String classSig = mirror.getClassSignature(location.classID);
+        return classSig.contains("$$Lambda$");
+      }
+
       private static boolean isLambdaMethod(VmMirror mirror, Location location) {
         String methodName = mirror.getMethodName(location.classID, location.methodID);
         return methodName.startsWith("lambda$");
diff --git a/src/test/java/com/android/tools/r8/desugar/LambdaHasNonSyntheticMethodTest.java b/src/test/java/com/android/tools/r8/desugar/LambdaHasNonSyntheticMethodTest.java
new file mode 100644
index 0000000..656cea6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/LambdaHasNonSyntheticMethodTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LambdaHasNonSyntheticMethodTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public LambdaHasNonSyntheticMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  interface MyCallable<T> {
+    T call() throws Exception;
+  }
+
+  static class TestClass {
+
+    private static void assertNotNull(Object o) {
+      if (o == null) throw new AssertionError();
+    }
+
+    private static void assertSame(Object o1, Object o2) {
+      if (o1 != o2) throw new AssertionError();
+    }
+
+    private static void assertFalse(boolean value) {
+      if (value) throw new AssertionError();
+    }
+
+    private static <T> void assertLambdaMethodIsNotSynthetic(T instance, Class<?> iface)
+        throws Exception {
+      Method ifaceMethod = null;
+      for (Method method : iface.getDeclaredMethods()) {
+        if (Modifier.isAbstract(method.getModifiers())) {
+          ifaceMethod = method;
+          break;
+        }
+      }
+      assertNotNull(ifaceMethod);
+      Method lambdaMethod =
+          instance.getClass().getMethod(ifaceMethod.getName(), ifaceMethod.getParameterTypes());
+      assertSame(ifaceMethod.getReturnType(), lambdaMethod.getReturnType());
+      assertSame(instance.getClass(), lambdaMethod.getDeclaringClass());
+      assertFalse(lambdaMethod.isSynthetic());
+      assertFalse(lambdaMethod.isBridge());
+    }
+
+    public static void main(String[] args) throws Exception {
+      StringBuilder builder = new StringBuilder("Hello, world");
+      MyCallable<String> instance = builder::toString;
+      assertLambdaMethodIsNotSynthetic(instance, MyCallable.class);
+      System.out.println(instance.call());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BufferedReaderTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BufferedReaderTest.java
index c2a8275..f71c916 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BufferedReaderTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/BufferedReaderTest.java
@@ -60,7 +60,7 @@
             : "Caught j$.io.UncheckedIOException");
   }
 
-  DesugaredLibraryConfiguration configurationWithBufferedReader(
+  DesugaredLibraryConfiguration configurationAlternative3(
       InternalOptions options, boolean libraryCompilation, TestParameters parameters) {
     // Parse the current configuration and amend the configuration for BufferedReader.lines. The
     // configuration is the same for both program and library.
@@ -69,28 +69,15 @@
             options.reporter,
             libraryCompilation,
             parameters.getApiLevel().getLevel())
-        .parse(
-            StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING),
-            builder -> {
-              if (parameters.getApiLevel().isLessThan(AndroidApiLevel.N)) {
-                builder.putRewritePrefix(
-                    "java.io.DesugarBufferedReader", "j$.io.DesugarBufferedReader");
-                builder.putRewritePrefix(
-                    "java.io.UncheckedIOException", "j$.io.UncheckedIOException");
-                builder.putRetargetCoreLibMember(
-                    "java.io.BufferedReader#lines", "java.io.DesugarBufferedReader");
-              }
-            });
+        .parse(StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING_ALTERNATIVE_3));
   }
 
   private void configurationForProgramCompilation(InternalOptions options) {
-    options.desugaredLibraryConfiguration =
-        configurationWithBufferedReader(options, false, parameters);
+    options.desugaredLibraryConfiguration = configurationAlternative3(options, false, parameters);
   }
 
   private void configurationForLibraryCompilation(InternalOptions options) {
-    options.desugaredLibraryConfiguration =
-        configurationWithBufferedReader(options, true, parameters);
+    options.desugaredLibraryConfiguration = configurationAlternative3(options, true, parameters);
   }
 
   @Test
@@ -156,7 +143,7 @@
         .addOptionsModification(
             options ->
                 options.desugaredLibraryConfiguration =
-                    configurationWithBufferedReader(options, false, parameters))
+                    configurationAlternative3(options, false, parameters))
         .addInnerClasses(BufferedReaderTest.class)
         .setMinApi(parameters.getApiLevel())
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
@@ -184,7 +171,7 @@
         .addOptionsModification(
             options ->
                 options.desugaredLibraryConfiguration =
-                    configurationWithBufferedReader(options, false, parameters))
+                    configurationAlternative3(options, false, parameters))
         .addInnerClasses(BufferedReaderTest.class)
         .addKeepMainRule(TestClass.class)
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
index b02b2e9..c7465ca 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
@@ -21,6 +21,8 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfigurationParser;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
@@ -67,6 +69,11 @@
     return buildDesugaredLibrary(apiLevel, "", false);
   }
 
+  protected Path buildDesugaredLibrary(
+      AndroidApiLevel apiLevel, Consumer<InternalOptions> optionsModifier) {
+    return buildDesugaredLibrary(apiLevel, "", false, ImmutableList.of(), optionsModifier);
+  }
+
   protected Path buildDesugaredLibrary(AndroidApiLevel apiLevel, String keepRules) {
     return buildDesugaredLibrary(apiLevel, keepRules, true);
   }
@@ -186,6 +193,21 @@
     return desugaredLib;
   }
 
+  protected DesugaredLibraryConfiguration configurationWithSupportAllCallbacksFromLibrary(
+      InternalOptions options,
+      boolean libraryCompilation,
+      TestParameters parameters,
+      boolean supportAllCallbacksFromLibrary) {
+    return new DesugaredLibraryConfigurationParser(
+            options.dexItemFactory(),
+            options.reporter,
+            libraryCompilation,
+            parameters.getApiLevel().getLevel())
+        .parse(
+            StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING),
+            builder -> builder.setSupportAllCallbacksFromLibrary(supportAllCallbacksFromLibrary));
+  }
+
   public interface KeepRuleConsumer extends StringConsumer {
 
     String get();
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionIntroduceInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionIntroduceInterfaceMethodTest.java
index 14f3ded..e90d8d2 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionIntroduceInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionIntroduceInterfaceMethodTest.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.function.Consumer;
@@ -34,6 +35,7 @@
 
   private final TestParameters parameters;
   private final boolean shrinkDesugaredLibrary;
+  private final boolean supportAllCallbacksFromLibrary;
 
   private static final AndroidApiLevel MIN_SUPPORTED = AndroidApiLevel.N;
   private static final String EXPECTED_RESULT =
@@ -42,16 +44,24 @@
           "forEach called",
           "action called from java consumer",
           "forEach called");
+  private static final String FAILING_EXPECTED_RESULT =
+      StringUtils.lines(
+          "action called from j$ consumer", "forEach called", "action called from java consumer");
   private static Path CUSTOM_LIB;
 
-  @Parameters(name = "{0}, shrinkDesugaredLibrary: {1}")
+  @Parameters(name = "{0}, shrink: {1}, supportCallbacks: {2}")
   public static List<Object[]> data() {
     return buildParameters(
-        getConversionParametersUpToExcluding(MIN_SUPPORTED), BooleanUtils.values());
+        getConversionParametersUpToExcluding(MIN_SUPPORTED),
+        BooleanUtils.values(),
+        BooleanUtils.values());
   }
 
   public ConversionIntroduceInterfaceMethodTest(
-      TestParameters parameters, boolean shrinkDesugaredLibrary) {
+      TestParameters parameters,
+      boolean shrinkDesugaredLibrary,
+      boolean supportAllCallbacksFromLibrary) {
+    this.supportAllCallbacksFromLibrary = supportAllCallbacksFromLibrary;
     this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
     this.parameters = parameters;
   }
@@ -75,6 +85,11 @@
         .addLibraryClasses(CustomLibClass.class)
         .addOptionsModification(opt -> opt.testing.trackDesugaredAPIConversions = true)
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .addOptionsModification(
+            opt ->
+                opt.desugaredLibraryConfiguration =
+                    configurationWithSupportAllCallbacksFromLibrary(
+                        opt, false, parameters, supportAllCallbacksFromLibrary))
         .compile()
         .inspect(this::assertDoubleForEach)
         .inspect(this::assertWrapperMethodsPresent)
@@ -85,11 +100,13 @@
             shrinkDesugaredLibrary)
         .addRunClasspathFiles(CUSTOM_LIB)
         .run(parameters.getRuntime(), Executor.class)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+        .apply(
+            r ->
+                r.assertSuccessWithOutput(
+                    supportAllCallbacksFromLibrary ? EXPECTED_RESULT : FAILING_EXPECTED_RESULT));
   }
 
   private void assertDoubleForEach(CodeInspector inspector) {
-    System.out.println(inspector.allClasses().size());
     FoundClassSubject myCollection =
         inspector.allClasses().stream()
             .filter(
@@ -103,7 +120,7 @@
             .collect(toSingle());
     assertEquals(
         "Missing duplicated forEach",
-        2,
+        supportAllCallbacksFromLibrary ? 2 : 1,
         IterableUtils.size(
             myCollection
                 .getDexProgramClass()
@@ -133,6 +150,11 @@
         .addKeepMainRule(Executor.class)
         .addLibraryClasses(CustomLibClass.class)
         .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .addOptionsModification(
+            opt ->
+                opt.desugaredLibraryConfiguration =
+                    configurationWithSupportAllCallbacksFromLibrary(
+                        opt, false, parameters, supportAllCallbacksFromLibrary))
         .compile()
         .inspect(this::assertDoubleForEach)
         .inspect(this::assertWrapperMethodsPresent)
@@ -143,7 +165,10 @@
             shrinkDesugaredLibrary)
         .addRunClasspathFiles(CUSTOM_LIB)
         .run(parameters.getRuntime(), Executor.class)
-        .assertSuccessWithOutput(EXPECTED_RESULT);
+        .apply(
+            r ->
+                r.assertSuccessWithOutput(
+                    supportAllCallbacksFromLibrary ? EXPECTED_RESULT : FAILING_EXPECTED_RESULT));
   }
 
   static class CustomLibClass {
@@ -197,7 +222,7 @@
 
     @Override
     public Iterator<E> iterator() {
-      return null;
+      return (Iterator<E>) Collections.singletonList(null).iterator();
     }
 
     @NotNull
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java
index 1f433d4..812cd34 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/DuplicateAPIDesugaredLibTest.java
@@ -6,55 +6,84 @@
 
 import static junit.framework.TestCase.assertEquals;
 
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime.DexRuntime;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class DuplicateAPIDesugaredLibTest extends DesugaredLibraryTestBase {
 
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private static final AndroidApiLevel MIN_SUPPORTED = AndroidApiLevel.N;
+
+  @Parameterized.Parameters(name = "{0}, shrinkDesugaredLibrary: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getConversionParametersUpToExcluding(MIN_SUPPORTED), BooleanUtils.values());
+  }
+
+  public DuplicateAPIDesugaredLibTest(TestParameters parameters, boolean shrinkDesugaredLibrary) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
   @Test
   public void testLib() throws Exception {
-    Box<Path> desugaredLibBox = new Box<>();
-    Path customLib =
-        testForD8()
-            .addProgramClasses(CustomLibClass.class)
-            .setMinApi(AndroidApiLevel.B)
-            .compile()
-            .writeToZip();
-    String stdOut =
-        testForD8()
-            .setMinApi(AndroidApiLevel.B)
-            .addProgramClasses(Executor.class)
-            .addLibraryClasses(CustomLibClass.class)
-            .enableCoreLibraryDesugaring(AndroidApiLevel.B)
-            .compile()
-            .addDesugaredCoreLibraryRunClassPath(
-                (AndroidApiLevel api) -> {
-                  desugaredLibBox.set(this.buildDesugaredLibrary(api));
-                  return desugaredLibBox.get();
-                },
-                AndroidApiLevel.B)
-            .addRunClasspathFiles(customLib)
-            .run(new DexRuntime(DexVm.ART_9_0_0_HOST), Executor.class)
-            .assertSuccess()
-            .getStdOut();
-    assertDupMethod(new CodeInspector(desugaredLibBox.get()));
-    assertLines2By2Correct(stdOut);
+    for (Boolean supportAllCallbacksFromLibrary : BooleanUtils.values()) {
+      Box<Path> desugaredLibBox = new Box<>();
+      Path customLib =
+          testForD8()
+              .addProgramClasses(CustomLibClass.class)
+              .setMinApi(AndroidApiLevel.B)
+              .compile()
+              .writeToZip();
+      String stdOut =
+          testForD8()
+              .setMinApi(AndroidApiLevel.B)
+              .addProgramClasses(Executor.class)
+              .addLibraryClasses(CustomLibClass.class)
+              .enableCoreLibraryDesugaring(AndroidApiLevel.B)
+              .compile()
+              .addDesugaredCoreLibraryRunClassPath(
+                  (AndroidApiLevel api) -> {
+                    desugaredLibBox.set(
+                        this.buildDesugaredLibrary(
+                            api,
+                            opt ->
+                                opt.desugaredLibraryConfiguration =
+                                    configurationWithSupportAllCallbacksFromLibrary(
+                                        opt, true, parameters, supportAllCallbacksFromLibrary)));
+                    return desugaredLibBox.get();
+                  },
+                  AndroidApiLevel.B)
+              .addRunClasspathFiles(customLib)
+              .run(new DexRuntime(DexVm.ART_9_0_0_HOST), Executor.class)
+              .assertSuccess()
+              .getStdOut();
+      assertDupMethod(new CodeInspector(desugaredLibBox.get()), supportAllCallbacksFromLibrary);
+      assertLines2By2Correct(stdOut);
+    }
   }
 
-  private void assertDupMethod(CodeInspector inspector) {
+  private void assertDupMethod(CodeInspector inspector, boolean supportAllCallbacksFromLibrary) {
     ClassSubject clazz = inspector.clazz("j$.util.concurrent.ConcurrentHashMap");
     assertEquals(
-        2,
+        supportAllCallbacksFromLibrary ? 2 : 1,
         clazz.virtualMethods().stream().filter(m -> m.getOriginalName().equals("forEach")).count());
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
index da7bcba..e7560e1f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/gson.cfg
@@ -14,11 +14,11 @@
 -dontwarn sun.misc.Unsafe
 
 # Application classes that will be serialized/deserialized over Gson
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$Data { <fields>; }
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentHashMap
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableHashMap
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableMap
--keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClasses$NullableConcurrentMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClass$Data { <fields>; }
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClass$NullableConcurrentHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClass$NullableHashMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClass$NullableMap
+-keep class com.android.tools.r8.desugar.desugaredlibrary.gson.AllMapsTestClass$NullableConcurrentMap
 -keep class com.android.tools.r8.desugar.desugaredlibrary.gson.OptionalTestClass$Data { <fields>; }
 
 # Prevent R8 from stripping interface information from TypeAdapter, TypeAdapterFactory,
@@ -28,6 +28,9 @@
 -keep class * implements com.google.gson.JsonSerializer
 -keep class * implements com.google.gson.JsonDeserializer
 
+# Prevent R8 from removing the generic signature of TypeToken
+-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
+
 # Prevent R8 from leaving Data object members always null
 -keepclassmembers,allowobfuscation class * {
   @com.google.gson.annotations.SerializedName <fields>;
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
index a04b0c1..5a788bf 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
@@ -57,11 +57,8 @@
 
   @BeforeClass
   public static void beforeAll() throws Exception {
-    assertThrowsWithHorizontalClassMerging(
-        () -> {
-          r8Lib11NoDesugar = compileR8(false);
-          r8Lib11Desugar = compileR8(true);
-        });
+    r8Lib11NoDesugar = compileR8(false);
+    r8Lib11Desugar = compileR8(true);
   }
 
   private static Path compileR8(boolean desugar) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java b/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
index da5dc5c..8256c29 100644
--- a/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/graph/initializedclasses/InitializedClassesInInstanceMethodsTest.java
@@ -11,6 +11,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -43,7 +44,6 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(InitializedClassesInInstanceMethodsTest.class)
         .addKeepMainRule(TestClass.class)
@@ -54,6 +54,7 @@
             })
         .allowAccessModification()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -137,6 +138,7 @@
     }
 
     @NeverClassInline
+    @NoHorizontalClassMerging
     static class B {
 
       @NeverInline
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
index b7bff95..d0242b4 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
@@ -29,7 +29,6 @@
     List<String> additionalProguardConfiguration =
         ImmutableList.of(
             ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "GmsCore_proguard.config");
-
     Map<String, IntSet> methodProcessingIds = new ConcurrentHashMap<>();
     AndroidApp app1 =
         buildAndTreeShakeFromDeployJar(
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
index dc2b296..1375f54 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2BuilderShrinkingTest.java
@@ -168,6 +168,10 @@
   }
 
   private void verifyBuildersAreAbsent(CodeInspector outputInspector) {
+    // TODO(b/171441793): Should be optimized out but fails do to soft pinning of super class.
+    if (true) {
+      return;
+    }
     assertThat(
         outputInspector.clazz(
             "com.android.tools.r8.proto2.Shrinking$HasFlaggedOffExtension$Builder"),
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
index 2eb4a58..17e3f64 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/PutObjectWithFinalizeTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -41,13 +42,13 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(PutObjectWithFinalizeTest.class)
         .addKeepMainRule(TestClass.class)
         // The class staticizer does not consider the finalize() method.
         .addOptionsModification(options -> options.enableClassStaticizer = false)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -148,5 +149,6 @@
 
   static class B extends A {}
 
+  @NoHorizontalClassMerging
   static class C {}
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
index bb36308..e5746ec 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
@@ -6,12 +6,16 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -31,12 +35,14 @@
 
   private Backend backend;
 
-  @Parameterized.Parameters(name = "Backend: {0}")
-  public static Backend[] data() {
-    return ToolHelper.getBackends();
+  @Parameterized.Parameters(name = "Backend: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParametersBuilder.builder().withNoneRuntime().build(), ToolHelper.getBackends());
   }
 
-  public MemberValuePropagationTest(TestBase.Backend backend) {
+  public MemberValuePropagationTest(TestParameters parameters, TestBase.Backend backend) {
+    parameters.assertNoneRuntime();
     this.backend = backend;
   }
 
@@ -56,14 +62,14 @@
   public void testWriteOnlyField_dontoptimize() throws Exception {
     CodeInspector inspector = runR8(DONT_OPTIMIZE);
     ClassSubject clazz = inspector.clazz(QUALIFIED_CLASS_NAME);
-    clazz.forAllMethods(
-        methodSubject -> {
-          // Dead code removal is not part of -dontoptimize. That is, even with -dontoptimize,
-          // field put instructions are gone with better dead code removal.
-          assertTrue(
-              methodSubject.streamInstructions().noneMatch(
-                  i -> i.isInstancePut() || i.isStaticPut()));
-        });
+    // With the support of 'allowshrinking' dontoptimize will now effectively pin all
+    // items that are not tree shaken out. The field operations will thus remain.
+    assertTrue(clazz.clinit().streamInstructions().anyMatch(InstructionSubject::isStaticPut));
+    assertTrue(
+        clazz
+            .uniqueInstanceInitializer()
+            .streamInstructions()
+            .anyMatch(InstructionSubject::isInstancePut));
   }
 
   private CodeInspector runR8(Path proguardConfig) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index 143f206..807b061 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -131,35 +131,32 @@
 
   @Before
   public void generateR8Version() throws Exception {
-    assertThrowsWithHorizontalClassMerging(
-        () -> {
-          outputDir = temp.newFolder().toPath();
-          Path mapFile = outputDir.resolve(DEFAULT_MAP_FILENAME);
-          generateR8Version(outputDir, mapFile, true);
-          String output;
-          if (parameters.isDexRuntime()) {
-            output =
-                ToolHelper.runArtNoVerificationErrors(
-                    Collections.singletonList(outputDir.resolve(DEFAULT_DEX_FILENAME).toString()),
-                    "inlining.Inlining",
-                    builder -> {},
-                    parameters.getRuntime().asDex().getVm());
-          } else {
-            assert parameters.isCfRuntime();
-            output =
-                ToolHelper.runJava(
-                        parameters.getRuntime().asCf(),
-                        Collections.singletonList("-noverify"),
-                        Collections.singletonList(outputDir),
-                        "inlining.Inlining")
-                    .stdout;
-          }
+    outputDir = temp.newFolder().toPath();
+    Path mapFile = outputDir.resolve(DEFAULT_MAP_FILENAME);
+    generateR8Version(outputDir, mapFile, true);
+    String output;
+    if (parameters.isDexRuntime()) {
+      output =
+          ToolHelper.runArtNoVerificationErrors(
+              Collections.singletonList(outputDir.resolve(DEFAULT_DEX_FILENAME).toString()),
+              "inlining.Inlining",
+              builder -> {},
+              parameters.getRuntime().asDex().getVm());
+    } else {
+      assert parameters.isCfRuntime();
+      output =
+          ToolHelper.runJava(
+                  parameters.getRuntime().asCf(),
+                  Collections.singletonList("-noverify"),
+                  Collections.singletonList(outputDir),
+                  "inlining.Inlining")
+              .stdout;
+    }
 
-          // Compare result with Java to make sure we have the same behavior.
-          ProcessResult javaResult = ToolHelper.runJava(getInputFile(), "inlining.Inlining");
-          assertEquals(0, javaResult.exitCode);
-          assertEquals(javaResult.stdout, output);
-        });
+    // Compare result with Java to make sure we have the same behavior.
+    ProcessResult javaResult = ToolHelper.runJava(getInputFile(), "inlining.Inlining");
+    assertEquals(0, javaResult.exitCode);
+    assertEquals(javaResult.stdout, output);
   }
 
   private void checkAbsentBooleanMethod(ClassSubject clazz, String name) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
index aa4dd9a..3ac7e29 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/InvokeVirtualWithRefinedReceiverTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -41,11 +42,11 @@
 
   @Test
   public void testR8() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(InvokeVirtualWithRefinedReceiverTest.class)
         .addKeepMainRule(MAIN)
         .enableNoVerticalClassMergingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .addOptionsModification(
@@ -148,6 +149,7 @@
   }
 
   @NoVerticalClassMerging
+  @NoHorizontalClassMerging
   @NeverClassInline
   static class C extends A {
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
index 3fb859c..04cbc5f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/callsites/WithStaticizerTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -39,12 +40,12 @@
 
   @Test
   public void testR8() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(WithStaticizerTest.class)
         .addKeepMainRule(MAIN)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutputLines("Input")
@@ -89,6 +90,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class Input {
     @NeverInline
     @Override
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/SingletonCanonicalizationWithApiLevelCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/SingletonCanonicalizationWithApiLevelCheckTest.java
index 7f5b905..bf78732 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/SingletonCanonicalizationWithApiLevelCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/SingletonCanonicalizationWithApiLevelCheckTest.java
@@ -62,15 +62,7 @@
         .compile()
         .addRunClasspathFiles(program)
         .run(parameters.getRuntime(), TestClass.class)
-        .apply(
-            runResult -> {
-              if (parameters.isCfRuntime()) {
-                // Constant canonicalization is disabled for CF.
-                runResult.assertSuccessWithEmptyOutput();
-              } else {
-                runResult.assertFailureWithErrorThatThrows(NoClassDefFoundError.class);
-              }
-            });
+        .assertSuccessWithEmptyOutput();
   }
 
   private List<String> getAssumeValuesRule(int version) {
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 c6f7772..8d73c3a 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
@@ -64,7 +64,6 @@
 
   @Test
   public void testTrivial() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     Class<?> main = TrivialTestClass.class;
     Class<?>[] classes = {
         TrivialTestClass.class,
@@ -85,6 +84,7 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableSideEffectAnnotations()
             .addKeepMainRule(main)
             .addKeepAttributes("LineNumberTable")
@@ -221,7 +221,6 @@
 
   @Test
   public void testInvalidatedRoot() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     Class<?> main = InvalidRootsTestClass.class;
     Class<?>[] classes = {
         InvalidRootsTestClass.class,
@@ -236,6 +235,7 @@
             .addProgramClasses(classes)
             .enableProguardTestOptions()
             .enableInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .addKeepMainRule(main)
             .addKeepAttributes("LineNumberTable")
             .addOptionsModification(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
index 226d995e..2b29cea 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/invalidroot/InvalidRootsTestClass.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.classinliner.invalidroot;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
 public class InvalidRootsTestClass {
   private static int ID = 0;
@@ -76,12 +77,14 @@
         prefix + ", " + (a == null ? "null" : a.foo()) + "): " + next());
   }
 
+  @NoHorizontalClassMerging
   public static class NeverReturnsNormally {
     public String foo() {
       throw new RuntimeException("NeverReturnsNormally::foo(): " + next());
     }
   }
 
+  @NoHorizontalClassMerging
   public static class InitNeverReturnsNormally {
     public InitNeverReturnsNormally() {
       throw new RuntimeException("InitNeverReturnsNormally::init(): " + next());
@@ -131,6 +134,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   public static class B {
     public String foo() {
       return "B::foo(" + next() + ")";
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
index 7e0239c..2fd1735 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.ir.optimize.classinliner.trivial;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+
+@NoHorizontalClassMerging
 public class ClassWithFinal {
   public String doNothing() {
     return "nothing at all";
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
index 287fe21..09b0ae3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/Regress131349148.java
@@ -62,8 +62,6 @@
                 ExistingException.class)
             .addKeepMainRule(TestClassCallingMethodWithNonExisting.class)
             .addKeepRules("-dontwarn " + NonExistingException.class.getTypeName())
-            .allowDiagnosticWarningMessages(
-                parameters.isCfRuntime() || parameters.getApiLevel().isLessThan(AndroidApiLevel.N))
             .setMinApi(parameters.getApiLevel())
             .compile()
             .assertAllWarningMessagesMatch(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
index 2012c6f..f5c7da8 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -54,6 +54,7 @@
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
         .enableSideEffectAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index df009b4..9d4c185 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -180,7 +180,6 @@
             "STATIC: String SimpleWithSideEffects.bar(String)",
             "STATIC: String SimpleWithSideEffects.foo()",
             "STATIC: String TrivialTestClass.next()",
-            "SimpleWithSideEffects SimpleWithSideEffects.INSTANCE",
             "SimpleWithSideEffects SimpleWithSideEffects.INSTANCE"),
         references(clazz, "testSimpleWithSideEffects", "void"));
 
@@ -268,7 +267,6 @@
 
   @Test
   public void testMoveToHost() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     Class<?> main = MoveToHostTestClass.class;
     Class<?>[] classes = {
         NeverInline.class,
@@ -287,6 +285,7 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableMemberValuePropagationAnnotations()
             .addKeepMainRule(main)
             .allowAccessModification()
@@ -315,7 +314,6 @@
             "STATIC: String movetohost.HostOkSideEffects.bar(String)",
             "STATIC: String movetohost.HostOkSideEffects.foo()",
             "STATIC: String movetohost.MoveToHostTestClass.next()",
-            "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE",
             "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE"),
         references(clazz, "testOkSideEffects", "void"));
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
index 4e5ce3a..cde24e8 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.movetohost;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class CandidateConflictMethod {
   @NeverInline
   public String foo() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
index 0834c000..eb8e2f6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.movetohost;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class CandidateOk {
   @NeverInline
   public String foo() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/typechecks/InstanceOfMethodSpecializationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/typechecks/InstanceOfMethodSpecializationTest.java
index 4173bb2..ed970af 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/typechecks/InstanceOfMethodSpecializationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/typechecks/InstanceOfMethodSpecializationTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -50,10 +51,10 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(InstanceOfMethodSpecializationTest.class)
         .addKeepMainRule(TestClass.class)
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
@@ -157,6 +158,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   public static class C extends A {
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
index e14bb7a..0de94c9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/PrivateInstanceMethodCollisionTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -48,7 +49,6 @@
 
   @Test
   public void b139769782() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     String expectedOutput = StringUtils.lines("A#foo(B)", "A#foo(B, Object)");
 
     if (parameters.isCfRuntime() && !minification && !allowAccessModification) {
@@ -63,6 +63,7 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .minification(minification)
         .allowAccessModification(allowAccessModification)
         .setMinApi(parameters.getRuntime())
@@ -122,6 +123,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class B {
     @Override
     public String toString() {
@@ -129,5 +131,6 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class C {}
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
new file mode 100644
index 0000000..3a795be
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
@@ -0,0 +1,47 @@
+// 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.kotlin.lambda;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
+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 KotlinLambdaMergingDebugTest extends AbstractR8KotlinTestBase {
+
+  private final TestParameters parameters;
+  private static final String FOLDER = "reprocess_merged_lambdas_kstyle";
+  private static final String MAIN_CLASS = "reprocess_merged_lambdas_kstyle.MainKt";
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public KotlinLambdaMergingDebugTest(TestParameters parameters) {
+    super(KotlinTargetVersion.JAVA_6);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testMergingKStyleLambdasAndReprocessingInDebug() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMode(CompilationMode.DEBUG)
+        .addProgramFiles(getKotlinJarFile(FOLDER))
+        .addProgramFiles(getJavaJarFile(FOLDER))
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(MAIN_CLASS)
+        .allowDiagnosticWarningMessages()
+        .compile()
+        .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
index 8102aaf..2811374 100644
--- a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
@@ -107,12 +107,13 @@
         "-assumemayhavesideeffects class adaptresourcefilenames.pkg.innerpkg.D {",
         "  void <init>();",
         "}",
-        "-neverclassinline class *");
+        "-neverclassinline class *",
+        "-nohorizontalclassmerging class adaptresourcefilenames.pkg.C",
+        "-nohorizontalclassmerging class adaptresourcefilenames.pkg.innerpkg.D");
   }
 
   @Test
   public void testEnabled() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting();
     compileWithR8(
         getProguardConfigWithNeverInline(true, null),
diff --git a/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java b/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
index 6ababf1..d56503c 100644
--- a/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
+++ b/src/test/java/com/android/tools/r8/naming/b124357885/B124357885Test.java
@@ -63,7 +63,7 @@
   @Test
   public void test() throws Exception {
     R8TestCompileResult compileResult =
-        testForR8(parameters.getBackend())
+        testForR8Compat(parameters.getBackend())
             .addProgramClasses(Main.class, Service.class, Foo.class, FooImpl.class)
             .addKeepMainRule(Main.class)
             .addKeepAttributes(
diff --git a/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java b/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
index bb27506..23271e2 100644
--- a/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
+++ b/src/test/java/com/android/tools/r8/naming/b130791310/B130791310.java
@@ -3,14 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming.b130791310;
 
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
@@ -108,45 +107,27 @@
     this.parameters = parameters;
   }
 
-  private void inspect(CodeInspector inspector, boolean isR8) {
+  private void inspect(CodeInspector inspector) {
     ClassSubject holder = inspector.clazz(SomeLogic.class);
     assertThat(holder, isPresentAndNotRenamed());
     MethodSubject someMethod = holder.uniqueMethodWithName("someMethod");
-    if (isR8) {
-      if (onlyForceInlining) {
-        assertThat(someMethod, isPresentAndNotRenamed());
-      } else {
-        assertThat(someMethod, not(isPresent()));
-      }
-    } else {
-      if (enableClassMerging) {
-        // Note that the method is not entirely gone, but merged to the implementer, along with some
-        // method signature modification.
-        assertThat(someMethod, not(isPresent()));
-      } else {
-        assertThat(someMethod, isPresentAndNotRenamed());
-      }
-    }
+    assertThat(someMethod, isPresentAndNotRenamed());
   }
 
   @Test
   public void testProguard() throws Exception {
     assumeFalse(onlyForceInlining);
     assumeTrue(parameters.isCfRuntime());
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(CLASSES)
         .addKeepClassAndMembersRules(MAIN)
         .addKeepRules(RULES)
         .addTestingAnnotationsAsProgramClasses()
         .setMinApi(parameters.getApiLevel())
-        .apply(
-            builder -> {
-              if (!enableClassMerging) {
-                builder.addKeepRules("-optimizations !class/merging/*");
-              }
-            })
+        .applyIf(
+            !enableClassMerging, builder -> builder.addKeepRules("-optimizations !class/merging/*"))
         .compile()
-        .inspect(inspector -> inspect(inspector, false));
+        .inspect(this::inspect);
   }
 
   @Test
@@ -158,14 +139,12 @@
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addOptionsModification(o -> o.enableVerticalClassMerging = enableClassMerging)
-        .apply(
-            builder -> {
-              if (onlyForceInlining) {
+        .applyIf(
+            onlyForceInlining,
+            builder ->
                 builder.addOptionsModification(
-                    o -> o.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE));
-              }
-            })
+                    o -> o.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE)))
         .compile()
-        .inspect(inspector -> inspect(inspector, true));
+        .inspect(this::inspect);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
index 0da2866..a64903b 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -16,6 +16,7 @@
 import com.google.common.base.Equivalence;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import org.hamcrest.Description;
@@ -58,6 +59,11 @@
       return this;
     }
 
+    public Builder map(int i, Function<StackTraceLine, StackTraceLine> map) {
+      stackTraceLines.set(i, map.apply(stackTraceLines.get(i)));
+      return this;
+    }
+
     public StackTrace build() {
       return new StackTrace(
           stackTraceLines,
@@ -123,6 +129,14 @@
       this.lineNumber = lineNumber;
     }
 
+    public Builder builderOf() {
+      return new Builder()
+          .setFileName(fileName)
+          .setClassName(className)
+          .setLineNumber(lineNumber)
+          .setMethodName(methodName);
+    }
+
     public static Builder builder() {
       return new Builder();
     }
diff --git a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
index e945182..65445a7 100644
--- a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
@@ -85,6 +85,7 @@
         .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
         .addKeepAttributes(ProguardKeepAttributes.INNER_CLASSES)
         .addKeepAttributes(ProguardKeepAttributes.ENCLOSING_METHOD)
+        .addKeepAllClassesRuleWithAllowObfuscation()
         .addKeepMainRule(Main.class)
         .addProgramClasses(Main.class)
         .addProgramClassesAndInnerClasses(A.class, B.class, CY.class, CYY.class)
diff --git a/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java b/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
index 58e43469..927decb 100644
--- a/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
+++ b/src/test/java/com/android/tools/r8/regress/b149890887/MissingLibraryTargetTest.java
@@ -62,7 +62,6 @@
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .allowDiagnosticWarningMessages(isDesugaring())
         .addProgramClasses(PROGRAM)
         .addKeepMainRule(MAIN)
         .addClasspathClasses(LIBRARY)
diff --git a/src/test/java/com/android/tools/r8/resolution/virtualtargets/PackagePrivateFinalOverrideInterfaceTest.java b/src/test/java/com/android/tools/r8/resolution/virtualtargets/PackagePrivateFinalOverrideInterfaceTest.java
new file mode 100644
index 0000000..4e8bdd8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/resolution/virtualtargets/PackagePrivateFinalOverrideInterfaceTest.java
@@ -0,0 +1,92 @@
+// 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.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.TestRunResult;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.resolution.virtualtargets.package_a.ViewModel;
+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/171369796.
+@RunWith(Parameterized.class)
+public class PackagePrivateFinalOverrideInterfaceTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public PackagePrivateFinalOverrideInterfaceTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(ViewModel.class, I.class, Zoolander.class, Main.class)
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertResult);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ViewModel.class, I.class, Zoolander.class, Main.class)
+        .addKeepClassAndMembersRules(ViewModel.class)
+        .addKeepClassAndMembersRules(I.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        .apply(this::assertResult);
+  }
+
+  public void assertResult(TestRunResult<?> runResult) {
+    if (parameters.isDexRuntime()
+        && parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+      runResult.assertFailureWithErrorThatMatches(containsString("overrides final"));
+    } else {
+      runResult.assertSuccessWithOutputLines("Zoolander::clear()");
+    }
+  }
+
+  public interface I {
+    void clear();
+  }
+
+  @NeverClassInline
+  public static class Zoolander extends ViewModel implements I {
+
+    @Override
+    @NeverInline
+    public final void clear() {
+      System.out.println("Zoolander::clear()");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      runClear(args.length == 0 ? new Zoolander() : null);
+    }
+
+    public static void runClear(I i) {
+      i.clear();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
index b8b1647..6c43c35 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceRegularExpressionTests.java
@@ -63,6 +63,7 @@
 
   @Test
   public void matchMultipleTypeNamesOnLine() {
+    // TODO(b/171691292): We are not supporting having multiple class matches for.
     runRetraceTest(
         "%c\\s%c\\s%c",
         new StackTraceForTest() {
@@ -79,7 +80,7 @@
 
           @Override
           public List<String> retracedStackTrace() {
-            return ImmutableList.of("AA.AA.AA BB.BB.BB CC.CC.CC");
+            return ImmutableList.of("AA.AA.AA b.b.b c.c.c");
           }
 
           @Override
@@ -91,6 +92,7 @@
 
   @Test
   public void matchMultipleSlashNamesOnLine() {
+    // TODO(b/171691292): We are not supporting having multiple class matches for.
     runRetraceTest(
         "%C\\s%C\\s%C",
         new StackTraceForTest() {
@@ -106,7 +108,7 @@
 
           @Override
           public List<String> retracedStackTrace() {
-            return ImmutableList.of("AA/AA BB/BB CC/CC");
+            return ImmutableList.of("AA/AA b/b/b c/c/c");
           }
 
           @Override
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
index edcb118..82d3915 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceTests.java
@@ -10,7 +10,6 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -136,15 +135,11 @@
 
   @Test
   public void testAmbiguousStackTrace() {
-    // TODO(b/170797525): Remove when we have a fixed ordering.
-    assumeTrue(useRegExpParsing);
     runRetraceTest(new AmbiguousStackTrace());
   }
 
   @Test
   public void testAmbiguousMissingLineStackTrace() {
-    // TODO(b/170797525): Remove when we have a fixed ordering.
-    assumeTrue(useRegExpParsing);
     runRetraceTest(new AmbiguousMissingLineStackTrace());
   }
 
@@ -190,8 +185,6 @@
 
   @Test
   public void testUnknownSourceStackTrace() {
-    // TODO(b/170797525): Remove when we have a fixed ordering.
-    assumeTrue(useRegExpParsing);
     runRetraceTest(new UnknownSourceStackTrace());
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java
index 3e5b4ca..2e75b1d 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceVerboseTests.java
@@ -6,7 +6,7 @@
 
 import static com.android.tools.r8.retrace.Retrace.DEFAULT_REGULAR_EXPRESSION;
 import static junit.framework.TestCase.assertEquals;
-import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
@@ -54,12 +54,12 @@
 
   @Test
   public void testAmbiguousMissingLineVerbose() {
+    // TODO(b/169346455): Enable when separated parser.
+    assumeFalse(useRegExpParsing);
     runRetraceTest(new AmbiguousWithSignatureVerboseStackTrace());
   }
 
   private TestDiagnosticMessagesImpl runRetraceTest(StackTraceForTest stackTraceForTest) {
-    // TODO(b/170293906): Remove assumption.
-    assumeTrue(useRegExpParsing);
     TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
     RetraceCommand retraceCommand =
         RetraceCommand.builder(diagnosticsHandler)
diff --git a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
index 1dfb3c9..45c103f 100644
--- a/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
+++ b/src/test/java/com/android/tools/r8/retrace/stacktraces/AmbiguousWithSignatureVerboseStackTrace.java
@@ -33,10 +33,10 @@
     return Arrays.asList(
         "java.lang.IndexOutOfBoundsException",
         "\tat java.util.ArrayList.get(ArrayList.java:411)",
-        "\tat com.android.tools.r8.Internal.void foo(int)(Internal.java)",
-        "\t<OR> at com.android.tools.r8.Internal.void foo(int,int)(Internal.java)",
+        "\tat com.android.tools.r8.Internal.boolean foo(int,int)(Internal.java)",
+        "\t<OR> at com.android.tools.r8.Internal.void foo(int)(Internal.java)",
         "\t<OR> at com.android.tools.r8.Internal.void foo(int,boolean)(Internal.java)",
-        "\t<OR> at com.android.tools.r8.Internal.boolean foo(int,int)(Internal.java)");
+        "\t<OR> at com.android.tools.r8.Internal.void foo(int,int)(Internal.java)");
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/shaking/EventuallyNonTargetedMethodTest.java b/src/test/java/com/android/tools/r8/shaking/EventuallyNonTargetedMethodTest.java
index b85fe68..24d4436 100644
--- a/src/test/java/com/android/tools/r8/shaking/EventuallyNonTargetedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/EventuallyNonTargetedMethodTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -37,10 +38,10 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .addInnerClasses(EventuallyNonTargetedMethodTest.class)
         .addKeepMainRule(Main.class)
@@ -72,6 +73,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   private static class C extends A {
 
     // Non-targeted override.
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index 184fdc9..fd676a3 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -183,7 +183,6 @@
 
   @Test
   public void testConditionalEqualsKeepClassMembers() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     GraphInspector referenceInspector =
         testForR8(Backend.CF)
             .enableGraphInspector()
diff --git a/src/test/java/com/android/tools/r8/shaking/NonTargetedMethodTest.java b/src/test/java/com/android/tools/r8/shaking/NonTargetedMethodTest.java
index 42dab36..2b2e440 100644
--- a/src/test/java/com/android/tools/r8/shaking/NonTargetedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/NonTargetedMethodTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -38,10 +39,10 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .addInnerClasses(NonTargetedMethodTest.class)
         .addKeepMainRule(Main.class)
@@ -72,6 +73,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   private static class C extends A {
 
     // Non-targeted override.
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java
new file mode 100644
index 0000000..f7e50d9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java
@@ -0,0 +1,147 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean allowOptimization;
+  private final boolean allowObfuscation;
+  private final Shrinker shrinker;
+
+  @Parameterized.Parameters(name = "{0}, opt:{1}, obf:{2}, {3}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest(
+      TestParameters parameters,
+      boolean allowOptimization,
+      boolean allowObfuscation,
+      Shrinker shrinker) {
+    this.parameters = parameters;
+    this.allowOptimization = allowOptimization;
+    this.allowObfuscation = allowObfuscation;
+    this.shrinker = shrinker;
+  }
+
+  String getExpected() {
+    return StringUtils.lines(
+        "A::foo",
+        // Reflective lookup of A::foo will only work if optimization and obfuscation are disabled.
+        Boolean.toString(!allowOptimization && !allowObfuscation),
+        "false");
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isR8()) {
+      run(testForR8(parameters.getBackend()));
+    } else {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    }
+  }
+
+  public <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    String keepRule =
+        "-if class * -keepclassmembers,allowshrinking"
+            + (allowOptimization ? ",allowoptimization" : "")
+            + (allowObfuscation ? ",allowobfuscation" : "")
+            + " class <1> { java.lang.String foo(); java.lang.String bar(); }";
+    builder
+        .addInnerClasses(ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules(keepRule)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class, A.class.getTypeName())
+        .assertSuccessWithOutput(getExpected())
+        .inspect(
+            inspector -> {
+              ClassSubject aClass = inspector.clazz(A.class);
+              ClassSubject bClass = inspector.clazz(B.class);
+              // The class constants will force A and B to be retained, but not the methods.
+              assertThat(bClass, isPresentAndRenamed());
+              assertThat(bClass.uniqueMethodWithName("foo"), not(isPresent()));
+              assertThat(bClass.uniqueMethodWithName("bar"), not(isPresent()));
+
+              assertThat(aClass, isPresentAndRenamed());
+              // The dependent rule with soft-pinning of bar never causes A::bar to be retained
+              // regardless of A and A::foo being retained.
+              assertThat(aClass.uniqueMethodWithName("bar"), not(isPresent()));
+              MethodSubject aFoo = aClass.uniqueMethodWithName("foo");
+              if (allowOptimization) {
+                assertThat(aFoo, not(isPresent()));
+              } else {
+                assertThat(aFoo, isPresentAndRenamed(allowObfuscation));
+                assertThat(inspector.clazz(TestClass.class).mainMethod(), invokesMethod(aFoo));
+              }
+            });
+  }
+
+  static class A {
+    public String foo() {
+      return "A::foo";
+    }
+
+    public String bar() {
+      return "A::bar";
+    }
+  }
+
+  static class B {
+    public String foo() {
+      return "B::foo";
+    }
+
+    public String bar() {
+      return "B::bar";
+    }
+  }
+
+  static class TestClass {
+
+    public static boolean hasFoo(String name) {
+      try {
+        return Class.forName(name).getDeclaredMethod("foo") != null;
+      } catch (Exception e) {
+        return false;
+      }
+    }
+
+    public static void main(String[] args) {
+      // Direct call to A.foo, if optimization is not allowed it will be kept.
+      A a = new A();
+      System.out.println(a.foo());
+      // Reference to A should not retain A::foo when allowoptimization is set.
+      // Note: if using class constant A.class, PG will actually retain A::foo !?
+      System.out.println(hasFoo(a.getClass().getTypeName()));
+      // Reference to B should not retain B::foo regardless of allowoptimization.
+      System.out.println(hasFoo(B.class.getTypeName()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepStaticMethodAllowShrinkingCompatibilityTest.java
similarity index 77%
copy from src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
copy to src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepStaticMethodAllowShrinkingCompatibilityTest.java
index ca30a1f..5c99afd 100644
--- a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepStaticMethodAllowShrinkingCompatibilityTest.java
@@ -2,7 +2,7 @@
 // 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.proguard;
+package com.android.tools.r8.shaking.allowshrinking;
 
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -10,13 +10,14 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -24,39 +25,48 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class AllowShrinkingCompatibilityTest extends TestBase {
+public class ConditionalKeepStaticMethodAllowShrinkingCompatibilityTest extends TestBase {
 
   private final boolean allowOptimization;
   private final TestParameters parameters;
-  private final ProguardVersion proguardVersion;
+  private final Shrinker shrinker;
 
   @Parameters(name = "{1}, {2}, allow optimization: {0}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         getTestParameters().withCfRuntimes().build(),
-        ProguardVersion.values());
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
   }
 
-  public AllowShrinkingCompatibilityTest(
-      boolean allowOptimization, TestParameters parameters, ProguardVersion proguardVersion) {
+  public ConditionalKeepStaticMethodAllowShrinkingCompatibilityTest(
+      boolean allowOptimization, TestParameters parameters, Shrinker shrinker) {
     this.allowOptimization = allowOptimization;
     this.parameters = parameters;
-    this.proguardVersion = proguardVersion;
+    this.shrinker = shrinker;
   }
 
   @Test
   public void test() throws Exception {
-    testForProguard(proguardVersion)
+    if (shrinker.isPG()) {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    } else {
+      run(testForR8(parameters.getBackend()));
+    }
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    builder
         .addProgramClasses(TestClass.class, Companion.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules(
-            "-keep,allowshrinking"
+            "-if class "
+                + Companion.class.getTypeName()
+                + " -keep,allowshrinking"
                 + (allowOptimization ? ",allowoptimization" : "")
                 + " class "
                 + Companion.class.getTypeName()
                 + " { <methods>; }")
-        .addDontWarn(getClass())
         .compile()
         .inspect(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
new file mode 100644
index 0000000..c9b3bc7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
@@ -0,0 +1,137 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepAllowShrinkingCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean allowOptimization;
+  private final boolean allowObfuscation;
+  private final Shrinker shrinker;
+
+  @Parameterized.Parameters(name = "{0}, opt:{1}, obf:{2}, {3}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public KeepAllowShrinkingCompatibilityTest(
+      TestParameters parameters,
+      boolean allowOptimization,
+      boolean allowObfuscation,
+      Shrinker shrinker) {
+    this.parameters = parameters;
+    this.allowOptimization = allowOptimization;
+    this.allowObfuscation = allowObfuscation;
+    this.shrinker = shrinker;
+  }
+
+  String getExpected() {
+    return StringUtils.lines(
+        "A::foo",
+        // Reflective lookup of A::foo will only work if optimization and obfuscation are disabled.
+        Boolean.toString(!allowOptimization && !allowObfuscation),
+        "false");
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isR8()) {
+      run(
+          testForR8(parameters.getBackend())
+              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
+              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+    } else {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    }
+  }
+
+  public <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    String keepRule =
+        "-keep,allowshrinking"
+            + (allowOptimization ? ",allowoptimization" : "")
+            + (allowObfuscation ? ",allowobfuscation" : "")
+            + " class * { java.lang.String foo(); }";
+    builder
+        .addInnerClasses(KeepAllowShrinkingCompatibilityTest.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules(keepRule)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class, A.class.getTypeName())
+        .assertSuccessWithOutput(getExpected())
+        .inspect(
+            inspector -> {
+              ClassSubject aClass = inspector.clazz(A.class);
+              ClassSubject bClass = inspector.clazz(B.class);
+              // The class constants will force A and B to be retained, but not the foo methods.
+              assertThat(bClass, isPresentAndRenamed(allowObfuscation));
+              assertThat(aClass, isPresentAndRenamed(allowObfuscation));
+              assertThat(bClass.uniqueMethodWithName("foo"), not(isPresent()));
+              MethodSubject aFoo = aClass.uniqueMethodWithName("foo");
+              if (allowOptimization) {
+                assertThat(aFoo, not(isPresent()));
+              } else {
+                assertThat(aFoo, isPresentAndRenamed(allowObfuscation));
+                assertThat(inspector.clazz(TestClass.class).mainMethod(), invokesMethod(aFoo));
+              }
+            });
+  }
+
+  static class A {
+    public String foo() {
+      return "A::foo";
+    }
+  }
+
+  static class B {
+    public String foo() {
+      return "B::foo";
+    }
+  }
+
+  static class TestClass {
+
+    public static boolean hasFoo(String name) {
+      try {
+        return Class.forName(name).getDeclaredMethod("foo") != null;
+      } catch (Exception e) {
+        return false;
+      }
+    }
+
+    public static void main(String[] args) {
+      // Direct call to A.foo, if optimization is not allowed it will be kept.
+      A a = new A();
+      System.out.println(a.foo());
+      // Reference to A should not retain A::foo when allowoptimization is set.
+      // Note: if using class constant A.class, PG will actually retain A::foo !?
+      System.out.println(hasFoo(a.getClass().getTypeName()));
+      // Reference to B should not retain B::foo regardless of allowoptimization.
+      System.out.println(hasFoo(B.class.getTypeName()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingIncludeDescriptorClassesCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingIncludeDescriptorClassesCompatibilityTest.java
new file mode 100644
index 0000000..44abb34
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingIncludeDescriptorClassesCompatibilityTest.java
@@ -0,0 +1,120 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeepAllowShrinkingIncludeDescriptorClassesCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final Shrinker shrinker;
+
+  @Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(), ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public KeepAllowShrinkingIncludeDescriptorClassesCompatibilityTest(
+      TestParameters parameters, Shrinker shrinker) {
+    this.parameters = parameters;
+    this.shrinker = shrinker;
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isPG()) {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    } else {
+      run(testForR8(parameters.getBackend()));
+    }
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    builder
+        .addProgramClasses(TestClass.class, A.class, B.class, SoftPinned.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules(
+            "-keepclassmembers,allowshrinking,includedescriptorclasses class "
+                + SoftPinned.class.getTypeName()
+                + " { <methods>; }")
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("true")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(TestClass.class), isPresent());
+
+              ClassSubject softPinnedClass = inspector.clazz(SoftPinned.class);
+              assertThat(softPinnedClass.uniqueMethodWithName("used"), isPresentAndNotRenamed());
+              assertThat(softPinnedClass.uniqueMethodWithName("unused"), not(isPresent()));
+
+              // SoftPinned.used(A) remains thus A must be present and not renamed.
+              assertThat(inspector.clazz(A.class), isPresentAndNotRenamed());
+
+              // TODO(b/171548534): Unexpectedly the behavior here is that the class B is also not
+              //  renamed. It appears that both R8 and PG will eagerly mark the name as not needing
+              //  to be  renamed.
+              assertThat(inspector.clazz(B.class), isPresentAndNotRenamed());
+            });
+  }
+
+  static class A {
+    @Override
+    public String toString() {
+      return getClass().getTypeName();
+    }
+  }
+
+  static class B {
+    @Override
+    public String toString() {
+      return getClass().getTypeName();
+    }
+  }
+
+  static class SoftPinned {
+
+    public static void used(A a) {
+      System.out.println(a.toString().endsWith(System.nanoTime() > 0 ? "A" : "junk"));
+    }
+
+    public static void unused(B b) {
+      System.out.println(b.toString().endsWith(System.nanoTime() > 0 ? "B" : "junk"));
+    }
+  }
+
+  static class TestClass {
+
+    // Kept helper so the classes A and B escape and prohibit removal.
+    public static void kept(Object o) {
+      if (System.nanoTime() < 0) {
+        System.out.println(o.toString());
+      }
+    }
+
+    public static void main(String[] args) {
+      A a = new A();
+      B b = new B();
+      kept(a);
+      kept(b);
+      SoftPinned.used(a);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java
new file mode 100644
index 0000000..8b01419
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassFieldsAllowShrinkingCompatibilityTest.java
@@ -0,0 +1,158 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.accessesField;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepClassFieldsAllowShrinkingCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean allowOptimization;
+  private final boolean allowObfuscation;
+  private final Shrinker shrinker;
+
+  @Parameterized.Parameters(name = "{0}, opt:{1}, obf:{2}, {3}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public KeepClassFieldsAllowShrinkingCompatibilityTest(
+      TestParameters parameters,
+      boolean allowOptimization,
+      boolean allowObfuscation,
+      Shrinker shrinker) {
+    this.parameters = parameters;
+    this.allowOptimization = allowOptimization;
+    this.allowObfuscation = allowObfuscation;
+    this.shrinker = shrinker;
+  }
+
+  String getExpected() {
+    return StringUtils.lines(
+        "A.foo",
+        // R8 will succeed in removing the field if allowoptimization is set.
+        Boolean.toString((shrinker.isPG() || !allowOptimization) && !allowObfuscation),
+        // R8 will always remove the unreferenced B.foo field.
+        Boolean.toString(shrinker.isPG() && !allowOptimization && !allowObfuscation));
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isR8()) {
+      run(
+          testForR8(parameters.getBackend())
+              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
+              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+    } else {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    }
+  }
+
+  public <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    String keepRule =
+        "-keepclassmembers,allowshrinking"
+            + (allowOptimization ? ",allowoptimization" : "")
+            + (allowObfuscation ? ",allowobfuscation" : "")
+            + " class * { java.lang.String foo; java.lang.String bar; }";
+    builder
+        .addInnerClasses(KeepClassFieldsAllowShrinkingCompatibilityTest.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules(keepRule)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class, A.class.getTypeName())
+        .assertSuccessWithOutput(getExpected())
+        .inspect(
+            inspector -> {
+              ClassSubject aClass = inspector.clazz(A.class);
+              ClassSubject bClass = inspector.clazz(B.class);
+              // The class constants will force A and B to be retained but renamed.
+              assertThat(aClass, isPresentAndRenamed());
+              assertThat(bClass, isPresentAndRenamed());
+
+              FieldSubject aFoo = aClass.uniqueFieldWithName("foo");
+              FieldSubject aBar = aClass.uniqueFieldWithName("bar");
+              FieldSubject bFoo = bClass.uniqueFieldWithName("foo");
+              FieldSubject bBar = bClass.uniqueFieldWithName("bar");
+
+              if (allowOptimization) {
+                // PG fails to optimize out the referenced field.
+                assertThat(aFoo, notIf(isPresent(), shrinker.isR8()));
+                assertThat(aBar, not(isPresent()));
+                assertThat(bFoo, not(isPresent()));
+                assertThat(bBar, not(isPresent()));
+              } else {
+                assertThat(aFoo, isPresentAndRenamed(allowObfuscation));
+                // TODO(b/171459868) It is inconsistent that the unused field A.bar is retained.
+                //   This does not match the R8 behavior for an unused method, so there may be an
+                //   optimization opportunity here.
+                //   (See KeepClassMethodsAllowShrinkingCompatibilityTest regarding methods).
+                assertThat(aBar, isPresentAndRenamed(allowObfuscation));
+                assertThat(inspector.clazz(TestClass.class).mainMethod(), accessesField(aFoo));
+                if (shrinker.isR8()) {
+                  assertThat(bFoo, not(isPresent()));
+                  assertThat(bBar, not(isPresent()));
+                } else {
+                  assertThat(bFoo, isPresentAndRenamed(allowObfuscation));
+                  assertThat(bBar, isPresentAndRenamed(allowObfuscation));
+                }
+              }
+            });
+  }
+
+  static class A {
+    // Note: If the fields are final PG actually allows itself to inline the values.
+    public String foo = "A.foo";
+    public String bar = "A.bar";
+  }
+
+  static class B {
+    public String foo = "B.foo";
+    public String bar = "B.bar";
+  }
+
+  static class TestClass {
+
+    public static boolean hasFoo(String name) {
+      try {
+        return Class.forName(name).getDeclaredField("foo") != null;
+      } catch (Exception e) {
+        return false;
+      }
+    }
+
+    public static void main(String[] args) {
+      // Conditional instance to prohibit class inlining of A.
+      A a = args.length == 42 ? null : new A();
+      // Direct use of A.foo, if optimization is not allowed it will be kept.
+      System.out.println(a.foo);
+      // Reference to A should not retain A.foo when allowoptimization is set.
+      System.out.println(hasFoo(a.getClass().getTypeName()));
+      // Reference to B should not retain B.foo regardless of allowoptimization.
+      System.out.println(hasFoo(B.class.getTypeName()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
new file mode 100644
index 0000000..e7d5191
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
@@ -0,0 +1,150 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepClassMethodsAllowShrinkingCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean allowOptimization;
+  private final boolean allowObfuscation;
+  private final Shrinker shrinker;
+
+  @Parameterized.Parameters(name = "{0}, opt:{1}, obf:{2}, {3}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(),
+        BooleanUtils.values(),
+        BooleanUtils.values(),
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
+  }
+
+  public KeepClassMethodsAllowShrinkingCompatibilityTest(
+      TestParameters parameters,
+      boolean allowOptimization,
+      boolean allowObfuscation,
+      Shrinker shrinker) {
+    this.parameters = parameters;
+    this.allowOptimization = allowOptimization;
+    this.allowObfuscation = allowObfuscation;
+    this.shrinker = shrinker;
+  }
+
+  String getExpected() {
+    return StringUtils.lines(
+        "A::foo",
+        // Reflective lookup of A::foo will only work if optimization and obfuscation are disabled.
+        Boolean.toString(!allowOptimization && !allowObfuscation),
+        "false");
+  }
+
+  @Test
+  public void test() throws Exception {
+    if (shrinker.isR8()) {
+      run(
+          testForR8(parameters.getBackend())
+              // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
+              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+    } else {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    }
+  }
+
+  public <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    String keepRule =
+        "-keepclassmembers,allowshrinking"
+            + (allowOptimization ? ",allowoptimization" : "")
+            + (allowObfuscation ? ",allowobfuscation" : "")
+            + " class * { java.lang.String foo(); java.lang.String bar(); }";
+    builder
+        .addInnerClasses(KeepClassMethodsAllowShrinkingCompatibilityTest.class)
+        .addKeepClassAndMembersRules(TestClass.class)
+        .addKeepRules(keepRule)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class, A.class.getTypeName())
+        .assertSuccessWithOutput(getExpected())
+        .inspect(
+            inspector -> {
+              ClassSubject aClass = inspector.clazz(A.class);
+              ClassSubject bClass = inspector.clazz(B.class);
+              // The class constants will force A and B to be retained, but not the methods.
+              assertThat(bClass, isPresentAndRenamed());
+              assertThat(bClass.uniqueMethodWithName("foo"), not(isPresent()));
+              assertThat(bClass.uniqueMethodWithName("bar"), not(isPresent()));
+
+              assertThat(aClass, isPresentAndRenamed());
+              // The dependent rule with soft-pinning of bar never causes A::bar to be retained
+              // regardless of A and A::foo being retained.
+              assertThat(aClass.uniqueMethodWithName("bar"), not(isPresent()));
+              MethodSubject aFoo = aClass.uniqueMethodWithName("foo");
+              if (allowOptimization) {
+                assertThat(aFoo, not(isPresent()));
+              } else {
+                assertThat(aFoo, isPresentAndRenamed(allowObfuscation));
+                assertThat(inspector.clazz(TestClass.class).mainMethod(), invokesMethod(aFoo));
+              }
+            });
+  }
+
+  static class A {
+    public String foo() {
+      return "A::foo";
+    }
+
+    public String bar() {
+      return "A::bar";
+    }
+  }
+
+  static class B {
+    public String foo() {
+      return "B::foo";
+    }
+
+    public String bar() {
+      return "B::bar";
+    }
+  }
+
+  static class TestClass {
+
+    public static boolean hasFoo(String name) {
+      try {
+        return Class.forName(name).getDeclaredMethod("foo") != null;
+      } catch (Exception e) {
+        return false;
+      }
+    }
+
+    public static void main(String[] args) {
+      // Direct call to A.foo, if optimization is not allowed it will be kept.
+      A a = new A();
+      System.out.println(a.foo());
+      // Reference to A should not retain A::foo when allowoptimization is set.
+      // Note: if using class constant A.class, PG will actually retain A::foo !?
+      System.out.println(hasFoo(a.getClass().getTypeName()));
+      // Reference to B should not retain B::foo regardless of allowoptimization.
+      System.out.println(hasFoo(B.class.getTypeName()));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepMethodNameCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepMethodNameCompatibilityTest.java
new file mode 100644
index 0000000..4266e57
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepMethodNameCompatibilityTest.java
@@ -0,0 +1,72 @@
+// 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.shaking.allowshrinking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.ProguardVersion;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestShrinkerBuilder;
+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 KeepMethodNameCompatibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public KeepMethodNameCompatibilityTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testProguard() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    test(testForProguard(ProguardVersion.getLatest()).addDontWarn(getClass()));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    test(testForR8(parameters.getBackend()));
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void test(T testBuilder) throws Exception {
+    testBuilder
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(
+            "-keepclassmembernames class " + TestClass.class.getTypeName() + " { void test(); }")
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector ->
+                assertThat(
+                    inspector.clazz(TestClass.class).uniqueMethodWithName("test"), isPresent()))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("foo");
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      test();
+    }
+
+    static void test() {
+      System.out.println("foo");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticFieldAllowShrinkingCompatibilityTest.java
similarity index 64%
copy from src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
copy to src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticFieldAllowShrinkingCompatibilityTest.java
index ca30a1f..2322f85 100644
--- a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticFieldAllowShrinkingCompatibilityTest.java
@@ -2,21 +2,23 @@
 // 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.proguard;
+package com.android.tools.r8.shaking.allowshrinking;
 
-import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.accessesField;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -24,30 +26,38 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class AllowShrinkingCompatibilityTest extends TestBase {
+public class KeepStaticFieldAllowShrinkingCompatibilityTest extends TestBase {
 
   private final boolean allowOptimization;
   private final TestParameters parameters;
-  private final ProguardVersion proguardVersion;
+  private final Shrinker shrinker;
 
   @Parameters(name = "{1}, {2}, allow optimization: {0}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         getTestParameters().withCfRuntimes().build(),
-        ProguardVersion.values());
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
   }
 
-  public AllowShrinkingCompatibilityTest(
-      boolean allowOptimization, TestParameters parameters, ProguardVersion proguardVersion) {
+  public KeepStaticFieldAllowShrinkingCompatibilityTest(
+      boolean allowOptimization, TestParameters parameters, Shrinker shrinker) {
     this.allowOptimization = allowOptimization;
     this.parameters = parameters;
-    this.proguardVersion = proguardVersion;
+    this.shrinker = shrinker;
   }
 
   @Test
   public void test() throws Exception {
-    testForProguard(proguardVersion)
+    if (shrinker.isPG()) {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    } else {
+      run(testForR8(parameters.getBackend()));
+    }
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    builder
         .addProgramClasses(TestClass.class, Companion.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules(
@@ -55,29 +65,29 @@
                 + (allowOptimization ? ",allowoptimization" : "")
                 + " class "
                 + Companion.class.getTypeName()
-                + " { <methods>; }")
-        .addDontWarn(getClass())
+                + " { <fields>; }")
         .compile()
         .inspect(
             inspector -> {
               ClassSubject testClassSubject = inspector.clazz(TestClass.class);
               assertThat(testClassSubject, isPresent());
 
-              ClassSubject companionClassSubject = inspector.clazz(Companion.class);
-              assertThat(companionClassSubject, notIf(isPresent(), allowOptimization));
-
               MethodSubject mainMethodSubject = testClassSubject.mainMethod();
-              MethodSubject getMethodSubject = companionClassSubject.uniqueMethodWithName("get");
+              ClassSubject companionClassSubject = inspector.clazz(Companion.class);
+              FieldSubject xFieldSubject = companionClassSubject.uniqueFieldWithName("x");
 
-              if (allowOptimization) {
+              // PG fails to optimize fields regardless of keep flags.
+              if (allowOptimization && shrinker.isR8()) {
+                assertThat(companionClassSubject, not(isPresent()));
                 assertTrue(
                     testClassSubject
                         .mainMethod()
                         .streamInstructions()
                         .allMatch(InstructionSubject::isReturnVoid));
               } else {
-                assertThat(mainMethodSubject, invokesMethod(getMethodSubject));
-                assertThat(getMethodSubject, isPresent());
+                assertThat(companionClassSubject, isPresent());
+                assertThat(mainMethodSubject, accessesField(xFieldSubject));
+                assertThat(xFieldSubject, isPresent());
               }
             })
         .run(parameters.getRuntime(), TestClass.class)
@@ -87,7 +97,7 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      if (Companion.get() != 42) {
+      if (Companion.x != 42) {
         System.out.println("Hello world!");
       }
     }
@@ -95,8 +105,6 @@
 
   static class Companion {
 
-    static int get() {
-      return 42;
-    }
+    static int x = 42;
   }
 }
diff --git a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticMethodAllowShrinkingCompatibilityTest.java
similarity index 80%
rename from src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
rename to src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticMethodAllowShrinkingCompatibilityTest.java
index ca30a1f..9dbdbb4 100644
--- a/src/test/java/com/android/tools/r8/proguard/AllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepStaticMethodAllowShrinkingCompatibilityTest.java
@@ -2,7 +2,7 @@
 // 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.proguard;
+package com.android.tools.r8.shaking.allowshrinking;
 
 import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -10,13 +10,14 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -24,30 +25,38 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class AllowShrinkingCompatibilityTest extends TestBase {
+public class KeepStaticMethodAllowShrinkingCompatibilityTest extends TestBase {
 
   private final boolean allowOptimization;
   private final TestParameters parameters;
-  private final ProguardVersion proguardVersion;
+  private final Shrinker shrinker;
 
   @Parameters(name = "{1}, {2}, allow optimization: {0}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         getTestParameters().withCfRuntimes().build(),
-        ProguardVersion.values());
+        ImmutableList.of(Shrinker.R8, Shrinker.PG));
   }
 
-  public AllowShrinkingCompatibilityTest(
-      boolean allowOptimization, TestParameters parameters, ProguardVersion proguardVersion) {
+  public KeepStaticMethodAllowShrinkingCompatibilityTest(
+      boolean allowOptimization, TestParameters parameters, Shrinker shrinker) {
     this.allowOptimization = allowOptimization;
     this.parameters = parameters;
-    this.proguardVersion = proguardVersion;
+    this.shrinker = shrinker;
   }
 
   @Test
   public void test() throws Exception {
-    testForProguard(proguardVersion)
+    if (shrinker.isPG()) {
+      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+    } else {
+      run(testForR8(parameters.getBackend()));
+    }
+  }
+
+  private <T extends TestShrinkerBuilder<?, ?, ?, ?, T>> void run(T builder) throws Exception {
+    builder
         .addProgramClasses(TestClass.class, Companion.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules(
@@ -56,7 +65,6 @@
                 + " class "
                 + Companion.class.getTypeName()
                 + " { <methods>; }")
-        .addDontWarn(getClass())
         .compile()
         .inspect(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/Shrinker.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/Shrinker.java
new file mode 100644
index 0000000..112fd05
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/Shrinker.java
@@ -0,0 +1,38 @@
+// 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.shaking.allowshrinking;
+
+import com.android.tools.r8.ProguardVersion;
+
+class Shrinker {
+
+  public static final Shrinker R8 = new Shrinker(null);
+  public static final Shrinker PG = new Shrinker(ProguardVersion.getLatest());
+  public static final Shrinker PG5 = new Shrinker(ProguardVersion.V5_2_1);
+  public static final Shrinker PG6 = new Shrinker(ProguardVersion.V6_0_1);
+  public static final Shrinker PG7 = new Shrinker(ProguardVersion.V7_0_0);
+
+  private final ProguardVersion proguardVersion;
+
+  private Shrinker(ProguardVersion proguardVersion) {
+    this.proguardVersion = proguardVersion;
+  }
+
+  boolean isR8() {
+    return proguardVersion == null;
+  }
+
+  boolean isPG() {
+    return proguardVersion != null;
+  }
+
+  public ProguardVersion getProguardVersion() {
+    return proguardVersion;
+  }
+
+  @Override
+  public String toString() {
+    return isR8() ? "r8" : "pg" + proguardVersion.getVersion();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationsOnFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationsOnFieldsTest.java
index d0e3b9e..55556b4 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationsOnFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationsOnFieldsTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -45,7 +46,6 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8Compat(backend)
         .enableNeverClassInliningAnnotations()
         .addProgramClasses(CLASSES)
@@ -55,6 +55,7 @@
             "-keepclassmembers class * { @**.*Annotation <fields>; }",
             "-keepattributes *Annotation*")
         .enableSideEffectAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .compile()
         .inspect(
             inspector -> {
@@ -92,6 +93,8 @@
 }
 
 class FieldAnnotationUse {}
+
+@NoHorizontalClassMerging
 class StaticFieldAnnotationUse {}
 
 @NeverClassInline
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java
new file mode 100644
index 0000000..eb29f88
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java
@@ -0,0 +1,141 @@
+// 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.shaking.assumenosideeffects;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.AssumeMayHaveSideEffects;
+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.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AssumeNoSideEffectsForJavaLangClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public AssumeNoSideEffectsForJavaLangClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableSideEffectAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+
+    inspectMethod(testClassSubject.uniqueMethodWithName("testModelingOfSideEffects"), false, false);
+    inspectMethod(
+        testClassSubject.uniqueMethodWithName("testModelingOfSideEffectsMaybeNull"), true, false);
+    inspectMethod(
+        testClassSubject.uniqueMethodWithName("testModelingOfSideEffectsMaybeSubclass"),
+        false,
+        true);
+  }
+
+  private void inspectMethod(
+      MethodSubject methodSubject, boolean maybeNullReceiver, boolean maybeSubtype) {
+    assertThat(methodSubject, isPresent());
+    assertThat(
+        methodSubject, onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("equals")));
+    assertThat(
+        methodSubject,
+        onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("hashCode")));
+    assertThat(methodSubject, onlyIf(maybeNullReceiver, invokesMethodWithName("getClass")));
+    assertThat(
+        methodSubject,
+        onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("toString")));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      testModelingOfSideEffects();
+      testModelingOfSideEffectsMaybeNull();
+      testModelingOfSideEffectsMaybeSubclass();
+    }
+
+    @AssumeMayHaveSideEffects
+    @NeverInline
+    static void testModelingOfSideEffects() {
+      Object o = new Object();
+      o.equals(new Object());
+      o.hashCode();
+      o.getClass();
+      o.toString();
+    }
+
+    @NeverInline
+    static void testModelingOfSideEffectsMaybeNull() {
+      createNullableObject().equals(new Object());
+      createNullableObject().hashCode();
+      createNullableObject().getClass();
+      createNullableObject().toString();
+    }
+
+    @NeverInline
+    static void testModelingOfSideEffectsMaybeSubclass() {
+      Object o = System.currentTimeMillis() > 0 ? new Object() : new A();
+      o.equals(new Object());
+      o.hashCode();
+      o.getClass();
+      o.toString();
+    }
+
+    static Object createNullableObject() {
+      return System.currentTimeMillis() > 0 ? new Object() : null;
+    }
+  }
+
+  static class A {
+
+    public A() {
+      super();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public int hashCode() {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public String toString() {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForLibraryMethodTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForLibraryMethodTest.java
index 632863a..9d74c64 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForLibraryMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForLibraryMethodTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.ArrayList;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,7 +37,8 @@
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class)
         .addKeepMainRule(TestClass.class)
-        .addKeepRules("-assumenosideeffects class java.lang.Object { int hashCode() return 42; }")
+        .addKeepRules(
+            "-assumenosideeffects class java.util.ArrayList { int hashCode() return 42; }")
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
@@ -58,7 +60,7 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      System.out.println(new Object().hashCode());
+      System.out.println(new ArrayList<>().hashCode());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationWithSuperCallTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationWithSuperCallTest.java
index ecf26e9..640fd85 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationWithSuperCallTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationWithSuperCallTest.java
@@ -15,13 +15,14 @@
 @RunWith(Parameterized.class)
 public class AssumenosideeffectsPropagationWithSuperCallTest extends TestBase {
   private static final Class<?> MAIN = DelegatesUser.class;
-  private static final String JVM_OUTPUT = StringUtils.lines(
-      "[Base] message1",
-      "The end"
-  );
-  private static final String OUTPUT_WITHOUT_MESSAGES = StringUtils.lines(
-      "The end"
-  );
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("[Base] message1", "[Base] message2", "The end");
+
+  // With horizontal class merging enabled the method body for debug is cleared, because the
+  // forwarded call to the class specific implementation has no side effects. The call to the
+  // function from main persists.
+  private static final String EXPECTED_OUTPUT_WITH_HORIZONTAL_CLASS_MERGING =
+      StringUtils.lines("[Base] message2", "The end");
 
   enum TestConfig {
     SPECIFIC_RULES,
@@ -46,11 +47,13 @@
       }
     }
 
-    public String expectedOutput() {
+    public String expectedOutput(TestParameters parameters) {
       switch (this) {
         case SPECIFIC_RULES:
         case NON_SPECIFIC_RULES_WITH_EXTENDS:
-          return JVM_OUTPUT;
+          return parameters.isCfRuntime()
+              ? EXPECTED_OUTPUT
+              : EXPECTED_OUTPUT_WITH_HORIZONTAL_CLASS_MERGING;
         default:
           throw new Unreachable();
       }
@@ -62,7 +65,8 @@
 
   @Parameterized.Parameters(name = "{0} {1}")
   public static Collection<Object[]> data() {
-    return buildParameters(getTestParameters().withAllRuntimes().build(), TestConfig.values());
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), TestConfig.values());
   }
 
   public AssumenosideeffectsPropagationWithSuperCallTest(
@@ -73,15 +77,14 @@
 
   @Test
   public void testR8() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(AssumenosideeffectsPropagationWithSuperCallTest.class)
         .addKeepMainRule(MAIN)
         .addKeepRules(config.getKeepRules())
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
-        .assertSuccessWithOutput(config.expectedOutput());
+        .assertSuccessWithOutput(config.expectedOutput(parameters));
   }
 
   static class BaseClass {
@@ -113,6 +116,10 @@
     public static void main(String... args) {
       BaseClass instance = createBase();
       instance.debug("message1");
+      if (System.currentTimeMillis() > 0) {
+        instance = new BaseClass();
+      }
+      instance.debug("message2");
       System.out.println("The end");
     }
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
index 5b0f344..8da32bc 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
@@ -7,17 +7,19 @@
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticPosition;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static java.util.Collections.emptyList;
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
@@ -25,14 +27,12 @@
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.List;
 import org.hamcrest.Matcher;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -54,53 +54,45 @@
     this.dontWarnObject = dontWarnObject;
   }
 
-  private void noCallToWait(CodeInspector inspector) {
-    ClassSubject classSubject = inspector.clazz(TestClass.class);
-    assertThat(classSubject, isPresent());
-    classSubject.forAllMethods(
-        foundMethodSubject ->
-            foundMethodSubject
-                .instructions(InstructionSubject::isInvokeVirtual)
-                .forEach(
-                    instructionSubject -> {
-                      Assert.assertNotEquals(
-                          "wait", instructionSubject.getMethod().name.toString());
-                    }));
+  private void checkIfWaitIsInvokedFromMain(CodeInspector inspector, boolean isR8) {
+    MethodSubject mainMethodSubject = inspector.clazz(TestClass.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertThat(
+        mainMethodSubject,
+        onlyIf(isR8, invokesMethod("void", "java.lang.Object", "wait", emptyList())));
   }
 
   private Matcher<Diagnostic> matchAssumeNoSideEffectsMessage() {
     return diagnosticMessage(
         containsString(
-            "The -assumenosideeffects rule matches methods on `java.lang.Object` with"
-                + " wildcards"));
+            "The -assumenosideeffects rule matches the following method(s) on java.lang.Object: "));
   }
 
   private Matcher<Diagnostic> matchMessageForAllProblematicMethods() {
     return diagnosticMessage(
         allOf(
-            containsString("void notify()"),
-            containsString("void notifyAll()"),
-            containsString("void wait()"),
-            containsString("void wait(long)"),
-            containsString("void wait(long, int)")));
+            containsString("notify()"),
+            containsString("notifyAll()"),
+            containsString("wait()"),
+            containsString("wait(long)"),
+            containsString("wait(long, int)")));
   }
 
   private Matcher<Diagnostic> matchMessageForWaitMethods() {
     return diagnosticMessage(
         allOf(
-            containsString("void wait()"),
-            containsString("void wait(long)"),
-            containsString("void wait(long, int)")));
+            containsString("wait()"),
+            containsString("wait(long)"),
+            containsString("wait(long, int)")));
   }
 
   private void assertErrorsOrWarnings(
       TestDiagnosticMessages diagnostics, List<Matcher<Diagnostic>> matchers) {
     if (dontWarnObject) {
+      diagnostics.assertNoMessages();
+    } else {
       diagnostics.assertOnlyWarnings();
       diagnostics.assertWarningsMatch(matchers);
-    } else {
-      diagnostics.assertOnlyErrors();
-      diagnostics.assertErrorsMatch(matchers);
     }
   }
 
@@ -115,23 +107,18 @@
         ImmutableList.of(
             allOf(matchAssumeNoSideEffectsMessage(), matchMessageForAllProblematicMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -169,30 +156,24 @@
                 diagnosticOrigin(methodsRuleOrigin),
                 diagnosticPosition(textRangeForString(methodsRule))));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .apply(
-              b ->
-                  b.getBuilder()
-                      .addProguardConfiguration(ImmutableList.of(starRule), starRuleOrigin))
-          .apply(
-              b ->
-                  b.getBuilder()
-                      .addProguardConfiguration(ImmutableList.of(methodsRule), methodsRuleOrigin))
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .apply(
+            b ->
+                b.getBuilder().addProguardConfiguration(ImmutableList.of(starRule), starRuleOrigin))
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addProguardConfiguration(ImmutableList.of(methodsRule), methodsRuleOrigin))
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -203,22 +184,9 @@
         .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
         .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { hash*(); }")
         .setMinApi(parameters.getApiLevel())
-        .allowDiagnosticWarningMessages(!dontWarnObject)
-        .compileWithExpectedDiagnostics(
-            diagnostics -> {
-              if (dontWarnObject) {
-                diagnostics.assertNoMessages();
-              } else {
-                diagnostics.assertOnlyWarnings();
-                diagnostics.assertWarningsMatch(
-                    allOf(
-                        matchAssumeNoSideEffectsMessage(),
-                        diagnosticMessage(containsString("int hashCode()"))));
-              }
-            })
+        .compile()
         .run(parameters.getRuntime(), TestClass.class)
-        // Code fails with exception if wait call is not removed.
-        .assertFailureWithErrorThatThrows(IllegalMonitorStateException.class);
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -227,23 +195,18 @@
         ImmutableList.of(
             allOf(matchAssumeNoSideEffectsMessage(), matchMessageForAllProblematicMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { <methods>; }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { <methods>; }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -251,36 +214,35 @@
     List<Matcher<Diagnostic>> matchers =
         ImmutableList.of(allOf(matchAssumeNoSideEffectsMessage(), matchMessageForWaitMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *** w*(...); }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *** w*(...); }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
   public void testR8WaitSpecificMethodMatch() throws Exception {
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
 
+    List<Matcher<Diagnostic>> matchers = ImmutableList.of(matchAssumeNoSideEffectsMessage());
+
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class java.lang.Object { void wait(); }")
         .setMinApi(parameters.getApiLevel())
-        .compile()
-        .inspect(this::noCallToWait)
+        .allowDiagnosticWarningMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Hello, world");
   }
@@ -313,7 +275,7 @@
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
     assumeTrue(parameters.isCfRuntime());
 
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
@@ -321,7 +283,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
-        .assertFailureWithErrorThatThrows(IllegalMonitorStateException.class);
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -329,24 +291,28 @@
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
     assumeTrue(parameters.isCfRuntime());
 
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class java.lang.Object { void wait(); }")
         .addKeepRules("-dontwarn " + B152492625.class.getTypeName())
         .setMinApi(parameters.getApiLevel())
         .compile()
-        .inspect(this::noCallToWait)
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, false))
         .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutputLines("Hello, world");
+        .assertSuccessWithOutput("Hello");
   }
 
   static class TestClass {
 
     public void m() throws Exception {
-      System.out.println("Hello, world");
-      // test fails if wait is not removed.
-      wait();
+      System.out.print("Hello");
+      // test fails if wait is removed.
+      try {
+        wait();
+      } catch (IllegalMonitorStateException e) {
+        System.out.println(", world");
+      }
     }
 
     public static void main(String[] args) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/shaking/assumevalues/AssumeValuesForLibraryMethodTest.java b/src/test/java/com/android/tools/r8/shaking/assumevalues/AssumeValuesForLibraryMethodTest.java
index 4d7e293..852409f 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumevalues/AssumeValuesForLibraryMethodTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumevalues/AssumeValuesForLibraryMethodTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.ArrayList;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,7 +37,7 @@
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class)
         .addKeepMainRule(TestClass.class)
-        .addKeepRules("-assumevalues class java.lang.Object { int hashCode() return 42; }")
+        .addKeepRules("-assumevalues class java.util.ArrayList { int hashCode() return 42; }")
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
@@ -58,7 +59,7 @@
   static class TestClass {
 
     public static void main(String[] args) {
-      System.out.println(new Object().hashCode());
+      System.out.println(new ArrayList<>().hashCode());
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/ClassSignaturesTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/ClassSignaturesTest.java
deleted file mode 100644
index 7ca532d..0000000
--- a/src/test/java/com/android/tools/r8/shaking/attributes/ClassSignaturesTest.java
+++ /dev/null
@@ -1,98 +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.shaking.attributes;
-
-import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.NeverClassInline;
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.shaking.ProguardKeepAttributes;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.util.Iterator;
-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 ClassSignaturesTest extends TestBase {
-
-  private final TestParameters parameters;
-  private final String EXPECTED = "Hello World!";
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  public ClassSignaturesTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void testRuntime() throws Exception {
-    testForRuntime(parameters)
-        .addProgramClasses(A.class, Main.class)
-        .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines(EXPECTED)
-        .inspect(this::inspect);
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .addProgramClasses(A.class, Main.class)
-        .addKeepMainRule(Main.class)
-        .setMinApi(parameters.getApiLevel())
-        .enableNeverClassInliningAnnotations()
-        .enableInliningAnnotations()
-        .addKeepAttributes(
-            ProguardKeepAttributes.SIGNATURE,
-            ProguardKeepAttributes.INNER_CLASSES,
-            ProguardKeepAttributes.ENCLOSING_METHOD)
-        .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines(EXPECTED)
-        .inspect(this::inspect);
-  }
-
-  private void inspect(CodeInspector inspector) {
-    ClassSubject aClass = inspector.clazz(A.class);
-    assertThat(aClass, isPresent());
-    assertEquals(
-        "Ljava/lang/Object;Ljava/lang/Iterable<"
-            + "Lcom/android/tools/r8/shaking/attributes/ClassSignaturesTest$Main;>;",
-        aClass.getFinalSignatureAttribute());
-  }
-
-  @NeverClassInline
-  public static class A implements Iterable<Main> {
-
-    @Override
-    @NeverInline
-    public Iterator<Main> iterator() {
-      throw new RuntimeException("FooBar");
-    }
-  }
-
-  public static class Main {
-
-    public static void main(String[] args) {
-      try {
-        new A().iterator();
-      } catch (RuntimeException e) {
-        if (!e.getMessage().contains("FooBar")) {
-          throw e;
-        }
-        System.out.println("Hello World!");
-      }
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
index b441e6b..05f9731 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
@@ -56,8 +56,7 @@
 
   @Test
   public void testKeptClassFieldAndMethodFull() throws Exception {
-    // TODO(b/170077516): The below should pass in false.
-    runTest(testForR8(parameters.getBackend()), true);
+    runTest(testForR8(parameters.getBackend()), false);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
index a3ad6d1..accb38b 100644
--- a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
@@ -7,16 +7,31 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class IncludeDescriptorClassesTest extends TestBase {
+  private final TestParameters testParameters;
+
+  public IncludeDescriptorClassesTest(TestParameters parameters) {
+    this.testParameters = parameters;
+  }
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
   private class Result {
     final CodeInspector inspector;
     final CodeInspector proguardedInspector;
@@ -26,7 +41,7 @@
       this.proguardedInspector = proguardedInspector;
     }
 
-    void assertKept(Class clazz) {
+    void assertKept(Class<?> clazz) {
       assertTrue(inspector.clazz(clazz.getCanonicalName()).isPresent());
       assertFalse(inspector.clazz(clazz.getCanonicalName()).isRenamed());
       if (proguardedInspector != null) {
@@ -35,14 +50,14 @@
     }
 
     // NOTE: 'synchronized' is supposed to disable inlining of this method.
-    synchronized void assertRemoved(Class clazz) {
+    synchronized void assertRemoved(Class<?> clazz) {
       assertFalse(inspector.clazz(clazz.getCanonicalName()).isPresent());
       if (proguardedInspector != null) {
         assertFalse(proguardedInspector.clazz(clazz).isPresent());
       }
     }
 
-    void assertRenamed(Class clazz) {
+    void assertRenamed(Class<?> clazz) {
       assertTrue(inspector.clazz(clazz.getCanonicalName()).isPresent());
       assertTrue(inspector.clazz(clazz.getCanonicalName()).isRenamed());
       if (proguardedInspector != null) {
@@ -59,22 +74,36 @@
           NativeReturnType.class,
           StaticFieldType.class,
           InstanceFieldType.class);
-  private List<Class> mainClasses = ImmutableList.of(
-      MainCallMethod1.class, MainCallMethod2.class, MainCallMethod3.class);
+  private List<Class<?>> mainClasses =
+      ImmutableList.of(MainCallMethod1.class, MainCallMethod2.class, MainCallMethod3.class);
 
-  Result runTest(Class mainClass, Path proguardConfig) throws Exception {
-    List<Class<?>> classes = new ArrayList<>(applicationClasses);
-    classes.add(mainClass);
+  Result runTest(ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> configure) throws Exception {
+    return runTest(configure, ignore -> {});
+  }
 
-    CodeInspector inspector = new CodeInspector(compileWithR8(classes, proguardConfig));
+  Result runTest(
+      ThrowableConsumer<TestShrinkerBuilder<?, ?, ?, ?, ?>> configure,
+      ThrowableConsumer<R8FullTestBuilder> configureR8)
+      throws Exception {
+    CodeInspector inspector =
+        testForR8(testParameters.getBackend())
+            .setMinApi(testParameters.getApiLevel())
+            .addProgramClasses(applicationClasses)
+            .apply(configure::accept)
+            .apply(configureR8)
+            .compile()
+            .inspector();
 
     CodeInspector proguardedInspector = null;
     // Actually running Proguard should only be during development.
     if (isRunProguard()) {
-      Path proguardedJar = temp.newFolder().toPath().resolve("proguarded.jar");
-      Path proguardedMap = temp.newFolder().toPath().resolve("proguarded.map");
-      ToolHelper.runProguard(jarTestClasses(classes), proguardedJar, proguardConfig, proguardedMap);
-      proguardedInspector = new CodeInspector(readJar(proguardedJar), proguardedMap);
+      proguardedInspector =
+          testForProguard()
+              .setMinApi(testParameters.getApiLevel())
+              .addProgramClasses(applicationClasses)
+              .apply(configure::accept)
+              .compile()
+              .inspector();
     }
 
     return new Result(inspector, proguardedInspector);
@@ -83,20 +112,21 @@
   @Test
   public void testNoIncludesDescriptorClasses() throws Exception {
     for (Class<?> mainClass : mainClasses) {
-      List<Class<?>> allClasses = new ArrayList<>(applicationClasses);
-      allClasses.add(mainClass);
+      List<String> proguardConfig =
+          ImmutableList.of(
+              "-keepclasseswithmembers class * {   ",
+              "  <fields>;                         ",
+              "  native <methods>;                 ",
+              "}                                   ",
+              "-allowaccessmodification            ");
 
-      Path proguardConfig = writeTextToTempFile(
-          keepMainProguardConfiguration(mainClass),
-          "-keepclasseswithmembers class * {   ",
-          "  <fields>;                         ",
-          "  native <methods>;                 ",
-          "}                                   ",
-          "-allowaccessmodification            ",
-          "-printmapping                       "
-      );
-
-      Result result = runTest(mainClass, proguardConfig);
+      Result result =
+          runTest(
+              builder ->
+                  builder
+                      .addProgramClasses(mainClass)
+                      .addKeepMainRule(mainClass)
+                      .addKeepRules(proguardConfig));
 
       result.assertKept(ClassWithNativeMethods.class);
       // Return types are not removed as they can be needed for verification.
@@ -113,17 +143,20 @@
   @Test
   public void testKeepClassesWithMembers() throws Exception {
     for (Class mainClass : mainClasses) {
-      Path proguardConfig = writeTextToTempFile(
-          keepMainProguardConfiguration(mainClass),
-          "-keepclasseswithmembers,includedescriptorclasses class * {  ",
-          "  <fields>;                                                 ",
-          "  native <methods>;                                         ",
-          "}                                                           ",
-          "-allowaccessmodification                                    ",
-          "-printmapping                                               "
-      );
-
-      Result result = runTest(mainClass, proguardConfig);
+      List<String> proguardConfig =
+          ImmutableList.of(
+              "-keepclasseswithmembers,includedescriptorclasses class * {  ",
+              "  <fields>;                                                 ",
+              "  native <methods>;                                         ",
+              "}                                                           ",
+              "-allowaccessmodification                                    ");
+      Result result =
+          runTest(
+              builder ->
+                  builder
+                      .addProgramClasses(mainClass)
+                      .addKeepMainRule(mainClass)
+                      .addKeepRules(proguardConfig));
 
       // With includedescriptorclasses return type, argument type ad field type are not renamed.
       result.assertKept(ClassWithNativeMethods.class);
@@ -137,17 +170,21 @@
   @Test
   public void testKeepClassMembers() throws Exception {
     for (Class mainClass : mainClasses) {
-      Path proguardConfig = writeTextToTempFile(
-          keepMainProguardConfiguration(mainClass),
-          "-keepclassmembers,includedescriptorclasses class * {  ",
-          "  <fields>;                                           ",
-          "  native <methods>;                                   ",
-          "}                                                     ",
-          "-allowaccessmodification                              ",
-          "-printmapping                                         "
-      );
+      List<String> proguardConfig =
+          ImmutableList.of(
+              "-keepclassmembers,includedescriptorclasses class * {  ",
+              "  <fields>;                                           ",
+              "  native <methods>;                                   ",
+              "}                                                     ",
+              "-allowaccessmodification                              ");
 
-      Result result = runTest(mainClass, proguardConfig);
+      Result result =
+          runTest(
+              builder ->
+                  builder
+                      .addProgramClasses(mainClass)
+                      .addKeepMainRule(mainClass)
+                      .addKeepRules(proguardConfig));
 
       // With includedescriptorclasses return type and argument type are not renamed.
       result.assertRenamed(ClassWithNativeMethods.class);
@@ -160,20 +197,23 @@
 
     @Test
     public void testKeepClassMemberNames() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     for (Class<?> mainClass : mainClasses) {
-        Path proguardConfig = writeTextToTempFile(
-            keepMainProguardConfiguration(mainClass),
-            // same as -keepclassmembers,allowshrinking,includedescriptorclasses
-            "-keepclassmembernames,includedescriptorclasses class * {  ",
-            "  <fields>;                                               ",
-            "  native <methods>;                                       ",
-            "}                                                         ",
-            "-allowaccessmodification                                  ",
-            "-printmapping                                             "
-        );
+      List<String> proguardConfig =
+          ImmutableList.of(
+              // same as -keepclassmembers,allowshrinking,includedescriptorclasses
+              "-keepclassmembernames,includedescriptorclasses class * {  ",
+              "  <fields>;                                               ",
+              "  native <methods>;                                       ",
+              "}                                                         ",
+              "-allowaccessmodification                                  ");
 
-        Result result = runTest(mainClass, proguardConfig);
+      Result result =
+          runTest(
+              builder ->
+                  builder
+                      .addProgramClasses(mainClass)
+                      .addKeepMainRule(mainClass)
+                      .addKeepRules(proguardConfig));
 
         boolean useNativeArgumentType =
             mainClass == MainCallMethod1.class || mainClass == MainCallMethod3.class;
diff --git a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/NativeReturnType.java b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/NativeReturnType.java
index f68a434..5290df7 100644
--- a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/NativeReturnType.java
+++ b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/NativeReturnType.java
@@ -4,6 +4,4 @@
 
 package com.android.tools.r8.shaking.includedescriptorclasses;
 
-public class NativeReturnType {
-
-}
+public class NativeReturnType {}
diff --git a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
index 908f9f7..104f2cd 100644
--- a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -37,13 +38,13 @@
 
   @Test
   public void test() throws Exception {
-    expectThrowsWithHorizontalClassMerging();
     testForR8(parameters.getBackend())
         .addInnerClasses(LibraryMethodOverrideTest.class)
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(options -> options.enableTreeShakingOfLibraryMethodOverrides = true)
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyOutput)
@@ -125,6 +126,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class DoesNotEscape {
 
     DoesNotEscape() {
@@ -139,6 +141,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class DoesNotEscapeWithSubThatDoesNotOverride {
 
     DoesNotEscapeWithSubThatDoesNotOverride() {
@@ -163,6 +166,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class DoesNotEscapeWithSubThatOverrides {
 
     DoesNotEscapeWithSubThatOverrides() {
@@ -195,6 +199,7 @@
   // EscapeWithSubThatOverridesAndEscapesSub is escaping from main(), unlike DoesNotEscapeWithSub-
   // ThatOverridesSub.
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class DoesNotEscapeWithSubThatOverridesAndEscapes {
 
     DoesNotEscapeWithSubThatOverridesAndEscapes() {
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index 578376e..e85f2d8 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.code.InvokeStaticRange;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.MoveResult;
+import com.android.tools.r8.code.MoveResultObject;
 import com.android.tools.r8.code.MoveResultWide;
 import com.android.tools.r8.code.Return;
 import com.android.tools.r8.code.ReturnObject;
@@ -42,7 +43,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.Assert;
@@ -1299,24 +1299,23 @@
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // The result from neither the div-int is never used.
-    MethodSignature signature = builder.addStaticMethod(
-        "void",
-        DEFAULT_METHOD_NAME,
-        ImmutableList.of("int", "int"),
-        1,
-        "    div-int             v0, p0, p1",
-        "    new-instance        v0, Ljava/lang/StringBuilder;",
-        "    invoke-direct       { v0 }, Ljava/lang/StringBuilder;-><init>()V",
-        "    return-void"
-    );
+    MethodSignature signature =
+        builder.addStaticMethod(
+            "java.lang.Object",
+            DEFAULT_METHOD_NAME,
+            ImmutableList.of("int", "int"),
+            1,
+            "    div-int             v0, p0, p1",
+            "    new-instance        v0, Ljava/lang/StringBuilder;",
+            "    invoke-direct       { v0 }, Ljava/lang/StringBuilder;-><init>()V",
+            "    return-object       v0");
 
     builder.addMainMethod(
         2,
         "    const               v0, 1",
         "    const               v1, 2",
-        "    invoke-static       { v0, v1 }, LTest;->method(II)V",
-        "    return-void"
-    );
+        "    invoke-static       { v0, v1 }, LTest;->method(II)Ljava/lang/Object;",
+        "    return-void");
 
     Consumer<InternalOptions> options =
         configureOptions(
@@ -1324,20 +1323,6 @@
               opts.outline.threshold = 1;
               opts.outline.minSize = 3;
               opts.outline.maxSize = 3;
-
-              // Do not allow dead code elimination of the new-instance and invoke-direct
-              // instructions.
-              // This can be achieved by not assuming that StringBuilder is present.
-              DexItemFactory dexItemFactory = opts.itemFactory;
-              dexItemFactory.libraryTypesAssumedToBePresent =
-                  new HashSet<>(dexItemFactory.libraryTypesAssumedToBePresent);
-              dexItemFactory.libraryTypesAssumedToBePresent.remove(
-                  dexItemFactory.stringBuilderType);
-              // ... and not assuming that StringBuilder.<init>() cannot have side effects.
-              dexItemFactory.libraryMethodsWithoutSideEffects =
-                  new IdentityHashMap<>(dexItemFactory.libraryMethodsWithoutSideEffects);
-              dexItemFactory.libraryMethodsWithoutSideEffects.remove(
-                  dexItemFactory.stringBuilderMethods.defaultConstructor);
             });
 
     AndroidApp originalApplication = buildApplicationWithAndroidJar(builder);
@@ -1347,9 +1332,10 @@
     // Return the processed method for inspection.
     DexEncodedMethod method = getMethod(processedApplication, signature);
     DexCode code = method.getCode().asDexCode();
-    assertEquals(2, code.instructions.length);
+    assertEquals(3, code.instructions.length);
     assertTrue(code.instructions[0] instanceof InvokeStatic);
-    assertTrue(code.instructions[1] instanceof ReturnVoid);
+    assertTrue(code.instructions[1] instanceof MoveResultObject);
+    assertTrue(code.instructions[2] instanceof ReturnObject);
     InvokeStatic invoke = (InvokeStatic) code.instructions[0];
     assertEquals(firstOutlineMethodName(), invoke.getMethod().qualifiedName());
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 86a21c1..ed4d158 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -6,7 +6,9 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import java.util.List;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeMatcher;
@@ -71,10 +73,93 @@
     };
   }
 
+  public static Matcher<MethodSubject> invokesMethod(
+      String returnType, String holderType, String methodName, List<String> parameterTypes) {
+    return new TypeSafeMatcher<MethodSubject>() {
+      @Override
+      protected boolean matchesSafely(MethodSubject subject) {
+        if (!subject.isPresent()) {
+          return false;
+        }
+        if (!subject.getMethod().hasCode()) {
+          return false;
+        }
+        return subject
+            .streamInstructions()
+            .anyMatch(isInvokeWithTarget(returnType, holderType, methodName, parameterTypes));
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        StringBuilder text =
+            new StringBuilder("invokes method `")
+                .append(returnType != null ? returnType : "*")
+                .append(" ")
+                .append(holderType != null ? holderType : "*")
+                .append(".")
+                .append(methodName != null ? methodName : "*")
+                .append("(");
+        if (parameterTypes != null) {
+          text.append(
+              parameterTypes.stream()
+                  .map(parameterType -> parameterType != null ? parameterType : "*")
+                  .collect(Collectors.joining(", ")));
+        } else {
+          text.append("...");
+        }
+        text.append(")`");
+        description.appendText(text.toString());
+      }
+
+      @Override
+      public void describeMismatchSafely(final MethodSubject subject, Description description) {
+        description.appendText("method did not");
+      }
+    };
+  }
+
+  public static Matcher<MethodSubject> invokesMethodWithName(String name) {
+    return invokesMethod(null, null, name, null);
+  }
+
   public static Predicate<InstructionSubject> isInvokeWithTarget(DexMethod target) {
     return instruction -> instruction.isInvoke() && instruction.getMethod() == target;
   }
 
+  public static Predicate<InstructionSubject> isInvokeWithTarget(
+      String returnType, String holderType, String methodName, List<String> parameterTypes) {
+    return instruction -> {
+      if (!instruction.isInvoke()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.getMethod();
+      if (returnType != null
+          && !invokedMethod.getReturnType().toSourceString().equals(returnType)) {
+        return false;
+      }
+      if (holderType != null
+          && !invokedMethod.getHolderType().toSourceString().equals(holderType)) {
+        return false;
+      }
+      if (methodName != null && !invokedMethod.getName().toSourceString().equals(methodName)) {
+        return false;
+      }
+      if (parameterTypes != null) {
+        if (parameterTypes.size() != invokedMethod.getArity()) {
+          return false;
+        }
+        for (int i = 0; i < parameterTypes.size(); i++) {
+          String parameterType = parameterTypes.get(i);
+          if (parameterType != null
+              && !invokedMethod.getParameter(i).toSourceString().equals(parameterType)) {
+            return false;
+          }
+        }
+      }
+      return true;
+    };
+  }
+
   public static Predicate<InstructionSubject> isFieldAccessWithTarget(DexField target) {
     return instruction -> instruction.isFieldAccess() && instruction.getField() == target;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 98c8562..29bebe1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -713,6 +713,10 @@
     }
   }
 
+  public static <T> Matcher<T> onlyIf(boolean condition, Matcher<T> matcher) {
+    return notIf(matcher, !condition);
+  }
+
   public static <T> Matcher<T> notIf(Matcher<T> matcher, boolean condition) {
     if (condition) {
       return not(matcher);
diff --git a/tools/r8_release.py b/tools/r8_release.py
index ce56269..cc4bebd 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -15,7 +15,7 @@
 
 import utils
 
-R8_DEV_BRANCH = '2.3'
+R8_DEV_BRANCH = '3.0'
 R8_VERSION_FILE = os.path.join(
     'src', 'main', 'java', 'com', 'android', 'tools', 'r8', 'Version.java')
 THIS_FILE_RELATIVE = os.path.join('tools', 'r8_release.py')
diff --git a/tools/test.py b/tools/test.py
index 64d2ae0..d7987a7 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -142,9 +142,6 @@
   result.add_option('--worktree',
       default=False, action='store_true',
       help='Tests are run in worktree and should not use gradle user home.')
-  result.add_option('--horizontal-class-merging', '--horizontal_class_merging',
-      help='Enable horizontal class merging.',
-      default=False, action='store_true')
   result.add_option('--runtimes',
       default=None,
       help='Test parameter runtimes to use, separated by : (eg, none:jdk9).'
@@ -297,9 +294,6 @@
     return_code = gradle.RunGradle(gradle_args, throw_on_failure=False)
     return archive_and_return(return_code, options)
 
-  if options.horizontal_class_merging:
-    gradle_args.append('-PhorizontalClassMerging')
-
   # Now run tests on selected runtime(s).
   if options.runtimes:
     if options.dex_vm != 'default':