Merge commit '9043b7c27b5c987737aa411f9ac562fda3254197' into dev-release
diff --git a/build.gradle b/build.gradle
index 77b5865..5c95cd6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -310,6 +310,7 @@
                 "android_jar/lib-v27",
                 "android_jar/lib-v28",
                 "android_jar/lib-v29",
+                "android_jar/lib-v30",
                 "core-lambda-stubs",
                 "dart-sdk",
                 "ddmlib",
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 615ca13..3c132e0 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -28,11 +28,13 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.PrefixRewritingNamingLens;
 import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
@@ -40,7 +42,10 @@
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -128,7 +133,7 @@
   /**
    * Command-line entry to D8.
    *
-   * See {@link D8Command#USAGE_MESSAGE} or run {@code d8 --help} for usage information.
+   * <p>See {@link D8Command#USAGE_MESSAGE} or run {@code d8 --help} for usage information.
    */
   public static void main(String[] args) {
     if (args.length == 0) {
@@ -215,10 +220,18 @@
       // Preserve markers from input dex code and add a marker with the current version
       // if there were class file inputs.
       boolean hasClassResources = false;
+      boolean hasDexResources = false;
       for (DexProgramClass dexProgramClass : app.classes()) {
         if (dexProgramClass.originatesFromClassResource()) {
           hasClassResources = true;
-          break;
+          if (hasDexResources) {
+            break;
+          }
+        } else if (dexProgramClass.originatesFromDexResource()) {
+          hasDexResources = true;
+          if (hasClassResources) {
+            break;
+          }
         }
       }
       Marker marker = options.getMarker(Tool.D8);
@@ -239,14 +252,30 @@
                 null)
             .write(options.getClassFileConsumer(), executor);
       } else {
+        NamingLens namingLens;
+        DexApplication finalApp = app;
+        if (!hasDexResources || !hasClassResources || !appView.rewritePrefix.isRewriting()) {
+          // All inputs are either dex or cf, or there is nothing to rewrite.
+          namingLens =
+              hasDexResources
+                  ? NamingLens.getIdentityLens()
+                  : PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
+        } else {
+          // There are both cf and dex inputs in the program, and rewriting is required for
+          // desugared library only on cf inputs. We cannot easily rewrite part of the program
+          // without iterating again the IR. We fall-back to writing one app with rewriting and
+          // merging it with the other app in rewriteNonDexInputs.
+          finalApp = rewriteNonDexInputs(appView, inputApp, options, executor, timing, app);
+          namingLens = NamingLens.getIdentityLens();
+        }
         new ApplicationWriter(
-                app,
-                null,
+                finalApp,
+                appView,
                 options,
                 marker == null ? null : ImmutableList.copyOf(markers),
                 GraphLense.getIdentityLense(),
                 InitClassLens.getDefault(),
-                PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView),
+                namingLens,
                 null)
             .write(executor);
       }
@@ -262,6 +291,57 @@
     }
   }
 
+  private static DexApplication rewriteNonDexInputs(
+      AppView<?> appView,
+      AndroidApp inputApp,
+      InternalOptions options,
+      ExecutorService executor,
+      Timing timing,
+      DexApplication app)
+      throws IOException, ExecutionException {
+    // TODO(b/154575955): Remove the naming lens in D8.
+    appView
+        .options()
+        .reporter
+        .warning(
+            new StringDiagnostic(
+                "The compilation is slowed down due to a mix of class file and dex file inputs in"
+                    + " the context of desugared library. This can be fixed by pre-compiling to"
+                    + " dex the class file inputs and dex merging only dex files."));
+    List<DexProgramClass> dexProgramClasses = new ArrayList<>();
+    List<DexProgramClass> nonDexProgramClasses = new ArrayList<>();
+    for (DexProgramClass aClass : app.classes()) {
+      if (aClass.originatesFromDexResource()) {
+        dexProgramClasses.add(aClass);
+      } else {
+        nonDexProgramClasses.add(aClass);
+      }
+    }
+    DexApplication cfApp = app.builder().replaceProgramClasses(nonDexProgramClasses).build();
+    ConvertedCfFiles convertedCfFiles = new ConvertedCfFiles();
+    new ApplicationWriter(
+            cfApp,
+            null,
+            options,
+            null,
+            GraphLense.getIdentityLense(),
+            InitClassLens.getDefault(),
+            PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView),
+            null,
+            convertedCfFiles)
+        .write(executor);
+    AndroidApp.Builder builder = AndroidApp.builder(inputApp);
+    builder.getProgramResourceProviders().clear();
+    builder.addProgramResourceProvider(convertedCfFiles);
+    AndroidApp newAndroidApp = builder.build();
+    DexApplication newApp = new ApplicationReader(newAndroidApp, options, timing).read(executor);
+    DexApplication.Builder<?> finalDexApp = newApp.builder();
+    for (DexProgramClass dexProgramClass : dexProgramClasses) {
+      finalDexApp.addProgramClass(dexProgramClass);
+    }
+    return finalDexApp.build();
+  }
+
   static DexApplication optimize(
       DexApplication application,
       AppInfo appInfo,
@@ -278,13 +358,35 @@
       if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
         System.out.print(printer.toString());
       } else {
-        try (OutputStreamWriter writer = new OutputStreamWriter(
-            new FileOutputStream(options.printCfgFile),
-            StandardCharsets.UTF_8)) {
+        try (OutputStreamWriter writer =
+            new OutputStreamWriter(
+                new FileOutputStream(options.printCfgFile), StandardCharsets.UTF_8)) {
           writer.write(printer.toString());
         }
       }
     }
     return application;
   }
+
+  static class ConvertedCfFiles implements DexIndexedConsumer, ProgramResourceProvider {
+
+    private final List<ProgramResource> resources = new ArrayList<>();
+
+    @Override
+    public synchronized void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      // TODO(b/154106502): Map Origin information.
+      resources.add(
+          ProgramResource.fromBytes(
+              Origin.unknown(), ProgramResource.Kind.DEX, data.copyByteData(), descriptors));
+    }
+
+    @Override
+    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+      return resources;
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {}
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 39ed0b5..17c6a0a 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -61,6 +61,7 @@
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.naming.SourceFileRewriter;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
+import com.android.tools.r8.optimize.BridgeHoisting;
 import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
@@ -99,6 +100,7 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 import java.io.ByteArrayOutputStream;
 import java.io.FileOutputStream;
@@ -290,8 +292,8 @@
       List<ProguardConfigurationRule> synthesizedProguardRules = new ArrayList<>();
       timing.begin("Strip unused code");
       Set<DexType> classesToRetainInnerClassAttributeFor = null;
+      Set<DexType> missingClasses = appView.appInfo().getMissingClasses();
       try {
-        Set<DexType> missingClasses = appView.appInfo().getMissingClasses();
         missingClasses = filterMissingClasses(
             missingClasses, options.getProguardConfiguration().getDontWarnPatterns());
         if (!missingClasses.isEmpty()) {
@@ -348,6 +350,9 @@
         assert appView.rootSet().verifyKeptTypesAreLive(appViewWithLiveness.appInfo());
         assert appView.rootSet().verifyKeptItemsAreKept(appView.appInfo().app(), appView.appInfo());
 
+        missingClasses =
+            Sets.union(missingClasses, appViewWithLiveness.appInfo().getMissingTypes());
+
         appView.rootSet().checkAllRulesAreUsed(options);
 
         if (options.proguardSeedsConsumer != null) {
@@ -636,7 +641,8 @@
             }
           }
 
-          Enqueuer enqueuer = EnqueuerFactory.createForFinalTreeShaking(appView, keptGraphConsumer);
+          Enqueuer enqueuer =
+              EnqueuerFactory.createForFinalTreeShaking(appView, keptGraphConsumer, missingClasses);
           appView.setAppInfo(
               enqueuer
                   .traceApplication(
@@ -681,6 +687,8 @@
                         pruner.getMethodsToKeepForConfigurationDebugging()));
             appView.setAppServices(appView.appServices().prunedCopy(removedClasses));
 
+            new BridgeHoisting(appViewWithLiveness).run();
+
             // TODO(b/130721661): Enable this assert.
             // assert Inliner.verifyNoMethodsInlinedDueToSingleCallSite(appView);
 
@@ -725,7 +733,7 @@
           // TODO(b/112437944): Avoid iterating the entire application to post-process every
           //  dynamicMethod() method.
           appView.withGeneratedMessageLiteShrinker(
-              shrinker -> shrinker.postOptimizeDynamicMethods(converter, timing));
+              shrinker -> shrinker.postOptimizeDynamicMethods(converter, executorService, timing));
 
           // If proto shrinking is enabled, we need to post-process every
           // findLiteExtensionByNumber() method. This ensures that there are no references to dead
@@ -733,7 +741,9 @@
           // TODO(b/112437944): Avoid iterating the entire application to post-process every
           //  findLiteExtensionByNumber() method.
           appView.withGeneratedExtensionRegistryShrinker(
-              shrinker -> shrinker.postOptimizeGeneratedExtensionRegistry(converter, timing));
+              shrinker ->
+                  shrinker.postOptimizeGeneratedExtensionRegistry(
+                      converter, executorService, timing));
         }
       }
 
@@ -858,12 +868,21 @@
               appView.dexItemFactory(), OptimizationFeedbackSimple.getInstance()));
     }
 
-    return appView.setAppInfo(
-        enqueuer.traceApplication(
-            appView.rootSet(),
-            options.getProguardConfiguration().getDontWarnPatterns(),
-            executorService,
-            timing));
+    AppView<AppInfoWithLiveness> appViewWithLiveness =
+        appView.setAppInfo(
+            enqueuer.traceApplication(
+                appView.rootSet(),
+                options.getProguardConfiguration().getDontWarnPatterns(),
+                executorService,
+                timing));
+    if (InternalOptions.assertionsEnabled()) {
+      // Register the dead proto types. These are needed to verify that no new missing types are
+      // reported and that no dead proto types are referenced in the generated application.
+      appViewWithLiveness.withProtoShrinker(
+          shrinker ->
+              shrinker.setDeadProtoTypes(appViewWithLiveness.appInfo().getDeadProtoTypes()));
+    }
+    return appViewWithLiveness;
   }
 
   static void processWhyAreYouKeepingAndCheckDiscarded(
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index c4c318c..79b4362 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -188,7 +188,7 @@
     this.programConsumer = consumer;
   }
 
-  private Iterable<VirtualFile> distribute(ExecutorService executorService)
+  private List<VirtualFile> distribute(ExecutorService executorService)
       throws ExecutionException, IOException {
     // Distribute classes into dex files.
     VirtualFile.Distributor distributor;
@@ -203,9 +203,7 @@
     } else {
       distributor = new VirtualFile.FillFilesDistributor(this, options, executorService);
     }
-
-    Iterable<VirtualFile> result = distributor.run();
-    return result;
+    return distributor.run();
   }
 
   /**
@@ -255,13 +253,18 @@
 
       // Generate the dex file contents.
       List<Future<Boolean>> dexDataFutures = new ArrayList<>();
-      Iterable<VirtualFile> virtualFiles = distribute(executorService);
+      List<VirtualFile> virtualFiles = distribute(executorService);
       if (options.encodeChecksums) {
         encodeChecksums(virtualFiles);
       }
       assert markers == null
           || markers.isEmpty()
           || application.dexItemFactory.extractMarkers() != null;
+      assert appView == null
+          || appView.withProtoShrinker(
+              shrinker ->
+                  virtualFiles.stream().allMatch(shrinker::verifyDeadProtoTypesNotReferenced),
+              true);
 
       // TODO(b/151313617): Sorting annotations mutates elements so run single threaded on main.
       SortAnnotations sortAnnotations = new SortAnnotations(namingLens);
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 4eee167..7d2dbdc 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -240,6 +240,14 @@
     transaction.commit();
   }
 
+  public boolean containsString(DexString string) {
+    return indexedItems.strings.contains(string);
+  }
+
+  public boolean containsType(DexType type) {
+    return indexedItems.types.contains(type);
+  }
+
   public boolean isEmpty() {
     return indexedItems.classes.isEmpty();
   }
@@ -258,7 +266,7 @@
       this.writer = writer;
     }
 
-    public abstract Collection<VirtualFile> run() throws ExecutionException, IOException;
+    public abstract List<VirtualFile> run() throws ExecutionException, IOException;
   }
 
   /**
@@ -277,7 +285,7 @@
     }
 
     @Override
-    public Collection<VirtualFile> run() {
+    public List<VirtualFile> run() {
       HashMap<DexProgramClass, VirtualFile> files = new HashMap<>();
       Collection<DexProgramClass> synthetics = new ArrayList<>();
       // Assign dedicated virtual files for all program classes.
@@ -468,7 +476,7 @@
     }
 
     @Override
-    public Collection<VirtualFile> run() throws IOException {
+    public List<VirtualFile> run() throws IOException {
       int totalClassNumber = classes.size();
       // First fill required classes into the main dex file.
       fillForMainDexList(classes);
@@ -531,7 +539,7 @@
     }
 
     @Override
-    public Collection<VirtualFile> run() throws ExecutionException, IOException {
+    public List<VirtualFile> run() throws ExecutionException, IOException {
       Map<FeatureSplit, Set<DexProgramClass>> featureSplitClasses =
           removeFeatureSplitClassesGetMapping();
       // Add all classes to the main dex file.
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 55c4ccb..3d9e26c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -135,12 +135,16 @@
   public boolean isSubtype(DexType subtype, DexType supertype) {
     assert subtype != null;
     assert supertype != null;
+    assert subtype.isClassType();
+    assert supertype.isClassType();
     return subtype == supertype || isStrictSubtypeOf(subtype, supertype);
   }
 
   public boolean isStrictSubtypeOf(DexType subtype, DexType supertype) {
     assert subtype != null;
     assert supertype != null;
+    assert subtype.isClassType();
+    assert supertype.isClassType();
     if (subtype == supertype) {
       return false;
     }
@@ -151,7 +155,6 @@
     if (supertype == dexItemFactory().objectType) {
       return true;
     }
-    // TODO(b/147658738): Clean up the code to not call on non-class types or fix this.
     if (!subtype.isClassType() || !supertype.isClassType()) {
       return false;
     }
@@ -166,12 +169,21 @@
         .shouldBreak();
   }
 
-  public boolean isRelatedBySubtyping(DexType type, DexType other) {
+  public boolean inSameHierarchy(DexType type, DexType other) {
     assert type.isClassType();
     assert other.isClassType();
     return isSubtype(type, other) || isSubtype(other, type);
   }
 
+  public boolean inDifferentHierarchy(DexType type1, DexType type2) {
+    return !inSameHierarchy(type1, type2);
+  }
+
+  public boolean isMissingOrHasMissingSuperType(DexType type) {
+    DexClass clazz = definitionFor(type);
+    return clazz == null || clazz.hasMissingSuperType(this);
+  }
+
   /** Collect all interfaces that this type directly or indirectly implements. */
   public Set<DexType> implementedInterfaces(DexType type) {
     assert type.isClassType();
@@ -265,6 +277,39 @@
     return relationChain;
   }
 
+  public boolean methodDefinedInInterfaces(DexEncodedMethod method, DexType implementingClass) {
+    DexClass holder = definitionFor(implementingClass);
+    if (holder == null) {
+      return false;
+    }
+    for (DexType iface : holder.interfaces.values) {
+      if (methodDefinedInInterface(method, iface)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public boolean methodDefinedInInterface(DexEncodedMethod method, DexType iface) {
+    DexClass potentialHolder = definitionFor(iface);
+    if (potentialHolder == null) {
+      return false;
+    }
+    assert potentialHolder.isInterface();
+    for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods()) {
+      if (virtualMethod.method.hasSameProtoAndName(method.method)
+          && virtualMethod.accessFlags.isSameVisibility(method.accessFlags)) {
+        return true;
+      }
+    }
+    for (DexType parentInterface : potentialHolder.interfaces.values) {
+      if (methodDefinedInInterface(method, parentInterface)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * Helper method used for emulated interface resolution (not in JVM specifications). The result
    * may be abstract.
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 4edc070..75ee97e 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_GROUP_CLASS_NAME_PREFIX;
 
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
@@ -147,11 +146,6 @@
   // Map from types to their subtyping information.
   private final Map<DexType, TypeInfo> typeInfo;
 
-  // Caches which static types that may store an object that has a non-default finalize() method.
-  // E.g., `java.lang.Object -> TRUE` if there is a subtype of Object that overrides finalize().
-  private final Map<DexType, Boolean> mayHaveFinalizeMethodDirectlyOrIndirectlyCache =
-      new ConcurrentHashMap<>();
-
   public AppInfoWithSubtyping(DirectMappedDexApplication application) {
     this(application, application.allClasses());
   }
@@ -357,51 +351,6 @@
     return true;
   }
 
-  public boolean isInstantiatedInterface(DexProgramClass clazz) {
-    assert checkIfObsolete();
-    return true; // Don't know, there might be.
-  }
-
-  public boolean methodDefinedInInterfaces(DexEncodedMethod method, DexType implementingClass) {
-    DexClass holder = definitionFor(implementingClass);
-    if (holder == null) {
-      return false;
-    }
-    for (DexType iface : holder.interfaces.values) {
-      if (methodDefinedInInterface(method, iface)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public boolean methodDefinedInInterface(DexEncodedMethod method, DexType iface) {
-    DexClass potentialHolder = definitionFor(iface);
-    if (potentialHolder == null) {
-      return false;
-    }
-    assert potentialHolder.isInterface();
-    for (DexEncodedMethod virtualMethod : potentialHolder.virtualMethods()) {
-      if (virtualMethod.method.hasSameProtoAndName(method.method)
-          && virtualMethod.accessFlags.isSameVisibility(method.accessFlags)) {
-        return true;
-      }
-    }
-    for (DexType parentInterface : potentialHolder.interfaces.values) {
-      if (methodDefinedInInterface(method, parentInterface)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public boolean isStringConcat(DexMethodHandle bootstrapMethod) {
-    assert checkIfObsolete();
-    return bootstrapMethod.type.isInvokeStatic()
-        && (bootstrapMethod.asMethod() == dexItemFactory().stringConcatWithConstantsMethod
-            || bootstrapMethod.asMethod() == dexItemFactory().stringConcatMethod);
-  }
-
   private void registerNewType(DexType newType, DexType superType) {
     assert checkIfObsolete();
     // Register the relationship between this type and its superType.
@@ -429,10 +378,6 @@
     return getTypeInfo(type).directSubtypes;
   }
 
-  public boolean isUnknown(DexType type) {
-    return getTypeInfo(type).isUnknown();
-  }
-
   public boolean hasSubtypes(DexType type) {
     return !getTypeInfo(type).directSubtypes.isEmpty();
   }
@@ -460,11 +405,6 @@
     return ImmutableList.of();
   }
 
-  public boolean isMissingOrHasMissingSuperType(DexType type) {
-    DexClass clazz = definitionFor(type);
-    return clazz == null || clazz.hasMissingSuperType(this);
-  }
-
   // TODO(b/139464956): Remove this method.
   public DexType getSingleSubtype_(DexType type) {
     TypeInfo info = getTypeInfo(type);
@@ -475,59 +415,4 @@
       return null;
     }
   }
-
-  public boolean inDifferentHierarchy(DexType type1, DexType type2) {
-    return !isSubtype(type1, type2) && !isSubtype(type2, type1);
-  }
-
-  public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(ClassTypeElement type) {
-    if (type.getClassType() == dexItemFactory().objectType && !type.getInterfaces().isEmpty()) {
-      for (DexType interfaceType : type.getInterfaces()) {
-        if (computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(interfaceType, false)) {
-          return true;
-        }
-      }
-      return false;
-    }
-    return computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(type.getClassType(), true);
-  }
-
-  private boolean computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(
-      DexType type, boolean lookUpwards) {
-    assert type.isClassType();
-    Boolean cache = mayHaveFinalizeMethodDirectlyOrIndirectlyCache.get(type);
-    if (cache != null) {
-      return cache;
-    }
-    DexClass clazz = definitionFor(type);
-    if (clazz == null) {
-      // This is strictly not conservative but is needed to avoid that we treat Object as having
-      // a subtype that has a non-default finalize() implementation.
-      mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, false);
-      return false;
-    }
-    if (clazz.isProgramClass()) {
-      if (lookUpwards) {
-        DexEncodedMethod resolutionResult =
-            resolveMethod(type, dexItemFactory().objectMembers.finalize).getSingleTarget();
-        if (resolutionResult != null && resolutionResult.isProgramMethod(this)) {
-          mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, true);
-          return true;
-        }
-      } else {
-        if (clazz.lookupVirtualMethod(dexItemFactory().objectMembers.finalize) != null) {
-          mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, true);
-          return true;
-        }
-      }
-    }
-    for (DexType subtype : allImmediateSubtypes(type)) {
-      if (computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(subtype, false)) {
-        mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, true);
-        return true;
-      }
-    }
-    mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, false);
-    return false;
-  }
 }
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 0e4b9cf..3a55054 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -27,7 +27,6 @@
 import com.google.common.base.Predicates;
 import java.util.IdentityHashMap;
 import java.util.Map;
-import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -252,8 +251,22 @@
     return protoShrinker;
   }
 
-  public void withGeneratedExtensionRegistryShrinker(
-      Consumer<GeneratedExtensionRegistryShrinker> consumer) {
+  public <E extends Throwable> void withProtoShrinker(ThrowingConsumer<ProtoShrinker, E> consumer)
+      throws E {
+    if (protoShrinker != null) {
+      consumer.accept(protoShrinker);
+    }
+  }
+
+  public <U> U withProtoShrinker(Function<ProtoShrinker, U> consumer, U defaultValue) {
+    if (protoShrinker != null) {
+      return consumer.apply(protoShrinker);
+    }
+    return defaultValue;
+  }
+
+  public <E extends Throwable> void withGeneratedExtensionRegistryShrinker(
+      ThrowingConsumer<GeneratedExtensionRegistryShrinker, E> consumer) throws E {
     if (protoShrinker != null && protoShrinker.generatedExtensionRegistryShrinker != null) {
       consumer.accept(protoShrinker.generatedExtensionRegistryShrinker);
     }
@@ -267,7 +280,8 @@
     return defaultValue;
   }
 
-  public void withGeneratedMessageLiteShrinker(Consumer<GeneratedMessageLiteShrinker> consumer) {
+  public <E extends Throwable> void withGeneratedMessageLiteShrinker(
+      ThrowingConsumer<GeneratedMessageLiteShrinker, E> consumer) throws E {
     if (protoShrinker != null && protoShrinker.generatedMessageLiteShrinker != null) {
       consumer.accept(protoShrinker.generatedMessageLiteShrinker);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index f8043c3..32e1d61 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -150,8 +150,8 @@
     methodCollection.addDirectMethods(methods);
   }
 
-  public void removeMethod(DexMethod method) {
-    methodCollection.removeMethod(method);
+  public DexEncodedMethod removeMethod(DexMethod method) {
+    return methodCollection.removeMethod(method);
   }
 
   public void setDirectMethods(DexEncodedMethod[] methods) {
@@ -559,7 +559,7 @@
     return getInitializer(DexType.EMPTY_ARRAY);
   }
 
-  public boolean hasMissingSuperType(AppInfoWithSubtyping appInfo) {
+  public boolean hasMissingSuperType(AppInfoWithClassHierarchy appInfo) {
     if (superType != null && appInfo.isMissingOrHasMissingSuperType(superType)) {
       return true;
     }
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 03f1415..28a0da2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -252,7 +252,6 @@
     this.code = code;
     this.classFileVersion = classFileVersion;
     this.d8R8Synthesized = d8R8Synthesized;
-
     assert code == null || !shouldNotHaveCode();
     assert parameterAnnotationsList != null;
   }
@@ -311,6 +310,10 @@
     return accessFlags.isFinal();
   }
 
+  public boolean isPublic() {
+    return accessFlags.isPublic();
+  }
+
   public boolean isInitializer() {
     checkIfObsolete();
     return isInstanceInitializer() || isClassInitializer();
@@ -1126,8 +1129,8 @@
     return new DexEncodedMethod(
         newMethod,
         newFlags,
-        target.annotations(),
-        target.parameterAnnotationsList,
+        DexAnnotationSet.empty(),
+        ParameterAnnotationsList.empty(),
         new SynthesizedCode(forwardSourceCodeBuilder::build),
         true);
   }
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 b2d7140..ffa702a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 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;
@@ -355,7 +356,8 @@
       createStaticallyKnownType(invocationHandlerDescriptor);
   public final DexType proxyType = createStaticallyKnownType(proxyDescriptor);
   public final DexType serviceLoaderType = createStaticallyKnownType(serviceLoaderDescriptor);
-
+  public final DexType serviceLoaderRewrittenClassType =
+      createStaticallyKnownType("L" + SERVICE_LOADER_CLASS_NAME + ";");
   public final DexType serviceLoaderConfigurationErrorType =
       createStaticallyKnownType(serviceLoaderConfigurationErrorDescriptor);
   public final DexType listType = createStaticallyKnownType(listDescriptor);
@@ -683,6 +685,7 @@
               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,
@@ -1809,6 +1812,10 @@
     }
   }
 
+  public boolean isPossiblyCompilerSynthesizedType(DexType type) {
+    return possibleCompilerSynthesizedTypes.contains(type);
+  }
+
   public void forEachPossiblyCompilerSynthesizedType(Consumer<DexType> fn) {
     possibleCompilerSynthesizedTypes.forEach(fn);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
index 23bfb99..e5f7c51 100644
--- a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -77,10 +78,10 @@
 
   public static final class EnumValueInfoMap {
 
-    private final Map<DexField, EnumValueInfo> map;
+    private final LinkedHashMap<DexField, EnumValueInfo> map;
 
-    public EnumValueInfoMap(Map<DexField, EnumValueInfo> map) {
-      this.map = ImmutableMap.copyOf(map);
+    public EnumValueInfoMap(LinkedHashMap<DexField, EnumValueInfo> map) {
+      this.map = map;
     }
 
     public int size() {
@@ -100,11 +101,11 @@
     }
 
     EnumValueInfoMap rewrittenWithLens(GraphLense lens) {
-      ImmutableMap.Builder<DexField, EnumValueInfo> builder = ImmutableMap.builder();
+      LinkedHashMap<DexField, EnumValueInfo> rewritten = new LinkedHashMap<>();
       map.forEach(
           (field, valueInfo) ->
-              builder.put(lens.lookupField(field), valueInfo.rewrittenWithLens(lens)));
-      return new EnumValueInfoMap(builder.build());
+              rewritten.put(lens.lookupField(field), valueInfo.rewrittenWithLens(lens)));
+      return new EnumValueInfoMap(rewritten);
     }
   }
 
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 ed2f612..ec3e102 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -181,7 +181,7 @@
     backing.addDirectMethods(methods);
   }
 
-  public void removeMethod(DexMethod method) {
+  public DexEncodedMethod removeMethod(DexMethod method) {
     DexEncodedMethod removed = backing.removeMethod(method);
     if (removed != null) {
       if (backing.belongsToDirectPool(removed)) {
@@ -191,6 +191,7 @@
         resetVirtualMethodCaches();
       }
     }
+    return removed;
   }
 
   public void setDirectMethods(DexEncodedMethod[] methods) {
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 5999bbf..34f32ff 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.shaking.InstantiationReason;
 import com.android.tools.r8.shaking.KeepReason;
 import com.android.tools.r8.utils.LensUtils;
+import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -21,6 +22,7 @@
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Provides information about all possibly instantiated classes and lambdas, their allocation sites,
@@ -140,6 +142,24 @@
       Consumer<DexProgramClass> onClass,
       Consumer<LambdaDescriptor> onLambda,
       AppInfo appInfo) {
+    traverseInstantiatedSubtypes(
+        type,
+        clazz -> {
+          onClass.accept(clazz);
+          return TraversalContinuation.CONTINUE;
+        },
+        lambda -> {
+          onLambda.accept(lambda);
+          return TraversalContinuation.CONTINUE;
+        },
+        appInfo);
+  }
+
+  public TraversalContinuation traverseInstantiatedSubtypes(
+      DexType type,
+      Function<DexProgramClass, TraversalContinuation> onClass,
+      Function<LambdaDescriptor, TraversalContinuation> onLambda,
+      AppInfo appInfo) {
     WorkList<DexClass> worklist = WorkList.newIdentityWorkList();
     if (type == appInfo.dexItemFactory().objectType) {
       // All types are below java.lang.Object, but we don't maintain an entry for it.
@@ -157,7 +177,12 @@
         // If no definition for the type is found, populate the worklist with any
         // instantiated subtypes and callback with any lambda instance.
         worklist.addIfNotSeen(instantiatedHierarchy.getOrDefault(type, Collections.emptySet()));
-        instantiatedLambdas.getOrDefault(type, Collections.emptyList()).forEach(onLambda);
+        for (LambdaDescriptor lambda :
+            instantiatedLambdas.getOrDefault(type, Collections.emptyList())) {
+          if (onLambda.apply(lambda).shouldBreak()) {
+            return TraversalContinuation.BREAK;
+          }
+        }
       } else {
         worklist.addIfNotSeen(initialClass);
       }
@@ -169,12 +194,20 @@
         DexProgramClass programClass = clazz.asProgramClass();
         if (isInstantiatedDirectly(programClass)
             || isInterfaceWithUnknownSubtypeHierarchy(programClass)) {
-          onClass.accept(programClass);
+          if (onClass.apply(programClass).shouldBreak()) {
+            return TraversalContinuation.BREAK;
+          }
         }
       }
       worklist.addIfNotSeen(instantiatedHierarchy.getOrDefault(clazz.type, Collections.emptySet()));
-      instantiatedLambdas.getOrDefault(clazz.type, Collections.emptyList()).forEach(onLambda);
+      for (LambdaDescriptor lambda :
+          instantiatedLambdas.getOrDefault(clazz.type, Collections.emptyList())) {
+        if (onLambda.apply(lambda).shouldBreak()) {
+          return TraversalContinuation.BREAK;
+        }
+      }
     }
+    return TraversalContinuation.CONTINUE;
   }
 
   public static class Builder extends ObjectAllocationInfoCollectionImpl {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
index 0bb8f21..be72b24 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/constant/SparseConditionalConstantPropagation.java
@@ -222,9 +222,14 @@
     } else if (jumpInstruction.isStringSwitch()) {
       StringSwitch switchInst = jumpInstruction.asStringSwitch();
       LatticeElement switchElement = getLatticeElement(switchInst.value());
-
-      // There is currently no constant propagation for strings.
-      assert !switchElement.isConst();
+      if (switchElement.isConst()) {
+        // There is currently no constant propagation for strings, so it must be null.
+        assert switchElement.asConst().getConstNumber().isZero();
+        BasicBlock target = switchInst.fallthroughBlock();
+        setExecutableEdge(jumpInstBlockNumber, target.getNumber());
+        flowEdges.add(target);
+        return;
+      }
     } else {
       assert jumpInstruction.isGoto() || jumpInstruction.isReturn() || jumpInstruction.isThrow();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 08b29ce..b4e025c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.analysis.proto;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.google.common.base.Predicates.not;
 
 import com.android.tools.r8.graph.AppView;
@@ -32,6 +33,7 @@
 import com.android.tools.r8.shaking.TreePrunerConfiguration;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.base.Predicates;
 import com.google.common.collect.Sets;
@@ -40,6 +42,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -156,26 +160,29 @@
     return removedExtensionFields.contains(field);
   }
 
-  public void postOptimizeGeneratedExtensionRegistry(IRConverter converter, Timing timing) {
+  public void postOptimizeGeneratedExtensionRegistry(
+      IRConverter converter, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
     timing.begin("[Proto] Post optimize generated extension registry");
-    forEachFindLiteExtensionByNumberMethod(
+    ThreadUtils.processItems(
+        this::forEachFindLiteExtensionByNumberMethod,
         method ->
             converter.processMethod(
                 method,
                 OptimizationFeedbackIgnore.getInstance(),
-                OneTimeMethodProcessor.getInstance()));
-    timing.end(); // [Proto] Post optimize generated extension registry
+                OneTimeMethodProcessor.getInstance()),
+        executorService);
+    timing.end();
   }
 
   private void forEachFindLiteExtensionByNumberMethod(Consumer<DexEncodedMethod> consumer) {
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (clazz.superType != references.extensionRegistryLiteType) {
-        continue;
-      }
-
-      for (DexEncodedMethod method : clazz.methods()) {
-        if (references.isFindLiteExtensionByNumberMethod(method.method)) {
-          consumer.accept(method);
+    for (DexType type : appView.appInfo().subtypes(references.extensionRegistryLiteType)) {
+      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+      if (clazz != null) {
+        for (DexEncodedMethod method : clazz.methods()) {
+          if (references.isFindLiteExtensionByNumberMethod(method.method)) {
+            consumer.accept(method);
+          }
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
index 0801f16..2914d30 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
@@ -20,8 +20,8 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.Switch;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.CallGraph.Node;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -36,7 +36,6 @@
 import com.android.tools.r8.ir.optimize.inliner.FixedInliningReasonStrategy;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.utils.PredicateSet;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -65,16 +64,11 @@
 
   /** Returns true if an action was deferred. */
   public boolean deferDeadProtoBuilders(
-      DexProgramClass clazz,
-      DexEncodedMethod context,
-      BooleanSupplier register,
-      Enqueuer enqueuer) {
+      DexProgramClass clazz, DexEncodedMethod context, BooleanSupplier register) {
     if (references.isDynamicMethod(context) && references.isGeneratedMessageLiteBuilder(clazz)) {
       if (register.getAsBoolean()) {
-        if (enqueuer.getMode().isFinalTreeShaking()) {
-          assert builders.getOrDefault(clazz, context) == context;
-          builders.put(clazz, context);
-        }
+        assert builders.getOrDefault(clazz, context) == context;
+        builders.put(clazz, context);
         return true;
       }
     }
@@ -263,7 +257,11 @@
     }
 
     @Override
-    public boolean switchCaseIsUnreachable(IntSwitch theSwitch, int index) {
+    public boolean switchCaseIsUnreachable(Switch theSwitch, int index) {
+      if (theSwitch.isStringSwitch()) {
+        assert false : "Unexpected string-switch instruction in dynamicMethod()";
+        return false;
+      }
       if (index != newBuilderOrdinal) {
         return false;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index b2d813a..40b8cb5 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.analysis.proto;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.getInfoValueFromMessageInfoConstructionInvoke;
 import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.getObjectsValueFromMessageInfoConstructionInvoke;
 import static com.android.tools.r8.ir.analysis.proto.ProtoUtils.setObjectsValueForMessageInfoConstructionInvoke;
@@ -11,7 +12,10 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoMessageInfo;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoObject;
 import com.android.tools.r8.ir.analysis.type.Nullability;
@@ -32,8 +36,11 @@
 import com.android.tools.r8.ir.conversion.OneTimeMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.function.Consumer;
 
 public class GeneratedMessageLiteShrinker {
@@ -70,22 +77,33 @@
     }
   }
 
-  public void postOptimizeDynamicMethods(IRConverter converter, Timing timing) {
+  public void postOptimizeDynamicMethods(
+      IRConverter converter, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
     timing.begin("[Proto] Post optimize dynamic methods");
-    forEachDynamicMethod(
+    ThreadUtils.processItems(
+        this::forEachDynamicMethod,
         method ->
             converter.processMethod(
                 method,
                 OptimizationFeedbackIgnore.getInstance(),
-                OneTimeMethodProcessor.getInstance()));
+                OneTimeMethodProcessor.getInstance()),
+        executorService);
     timing.end();
   }
 
   private void forEachDynamicMethod(Consumer<DexEncodedMethod> consumer) {
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      DexEncodedMethod dynamicMethod = clazz.lookupVirtualMethod(references::isDynamicMethod);
-      if (dynamicMethod != null) {
-        consumer.accept(dynamicMethod);
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    for (DexType type : appView.appInfo().subtypes(references.generatedMessageLiteType)) {
+      DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+      if (clazz != null) {
+        DexMethod dynamicMethod =
+            dexItemFactory.createMethod(
+                type, references.dynamicMethodProto, references.dynamicMethodName);
+        DexEncodedMethod encodedDynamicMethod = clazz.lookupVirtualMethod(dynamicMethod);
+        if (encodedDynamicMethod != null) {
+          consumer.accept(encodedDynamicMethod);
+        }
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoShrinker.java
index b7b8ef1..78f9e6e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoShrinker.java
@@ -4,9 +4,14 @@
 
 package com.android.tools.r8.ir.analysis.proto;
 
+import com.android.tools.r8.dex.VirtualFile;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoFieldTypeFactory;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
+import java.util.Set;
 
 public class ProtoShrinker {
 
@@ -17,6 +22,8 @@
   public final GeneratedMessageLiteBuilderShrinker generatedMessageLiteBuilderShrinker;
   public final ProtoReferences references;
 
+  private Set<DexType> deadProtoTypes = Sets.newIdentityHashSet();
+
   public ProtoShrinker(AppView<AppInfoWithLiveness> appView) {
     ProtoFieldTypeFactory factory = new ProtoFieldTypeFactory();
     ProtoReferences references = new ProtoReferences(appView.dexItemFactory());
@@ -36,4 +43,22 @@
             : null;
     this.references = references;
   }
+
+  public Set<DexType> getDeadProtoTypes() {
+    return deadProtoTypes;
+  }
+
+  public void setDeadProtoTypes(Set<DexType> deadProtoTypes) {
+    // We should only need to keep track of the dead proto types for assertion purposes.
+    InternalOptions.checkAssertionsEnabled();
+    this.deadProtoTypes = deadProtoTypes;
+  }
+
+  public boolean verifyDeadProtoTypesNotReferenced(VirtualFile virtualFile) {
+    for (DexType deadProtoType : deadProtoTypes) {
+      assert !virtualFile.containsString(deadProtoType.descriptor);
+      assert !virtualFile.containsType(deadProtoType);
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 9dea6f8..4bd58c4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -253,6 +253,13 @@
     return successors.size();
   }
 
+  public int numberOfExceptionalSuccessors() {
+    if (hasCatchHandlers()) {
+      return catchHandlers.getUniqueTargets().size();
+    }
+    return 0;
+  }
+
   public boolean hasUniquePredecessor() {
     return predecessors.size() == 1;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 1537f85..32c5602 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -223,6 +224,16 @@
   }
 
   @Override
+  public Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value) {
+    ConstString constStringInstruction = code.createStringConstant(appView, value);
+    // Note that we only keep position info for throwing instructions in release mode.
+    constStringInstruction.setPosition(
+        appView.options().debug ? current.getPosition() : Position.none());
+    add(constStringInstruction);
+    return constStringInstruction.outValue();
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     if (current == null) {
       throw new IllegalStateException();
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 06aef0a..fcb0c1b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -43,6 +43,10 @@
     this.value = value;
   }
 
+  public static ConstNumber asConstNumberOrNull(Instruction instruction) {
+    return (ConstNumber) instruction;
+  }
+
   @Override
   public int opcode() {
     return Opcodes.CONST_NUMBER;
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 268c065..064842a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -3,11 +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.ir.analysis.type.Nullability.definitelyNotNull;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.analysis.TypeChecker;
@@ -15,6 +18,7 @@
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.BasicBlock.ThrowingInfo;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.origin.Origin;
@@ -29,6 +33,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -982,6 +987,10 @@
     return this::instructionIterator;
   }
 
+  public Stream<Instruction> streamInstructions() {
+    return Streams.stream(instructions());
+  }
+
   public <T extends Instruction> Iterable<T> instructions(Predicate<Instruction> predicate) {
     return () -> IteratorUtils.filter(instructionIterator(), predicate);
   }
@@ -1098,6 +1107,16 @@
     return new ConstNumber(out, value);
   }
 
+  public ConstString createStringConstant(AppView<?> appView, DexString value) {
+    return createStringConstant(appView, value, null);
+  }
+
+  public ConstString createStringConstant(
+      AppView<?> appView, DexString value, DebugLocalInfo local) {
+    Value out = createValue(TypeElement.stringClassType(appView, definitelyNotNull()), local);
+    return new ConstString(out, value, ThrowingInfo.defaultForConstString(appView.options()));
+  }
+
   public Phi createPhi(BasicBlock block, TypeElement type) {
     return new Phi(valueNumberGenerator.next(), block, type, null, RegisterReadType.NORMAL);
   }
@@ -1107,8 +1126,7 @@
   }
 
   public ConstClass createConstClass(AppView<?> appView, DexType type) {
-    Value out =
-        createValue(TypeElement.fromDexType(type, Nullability.definitelyNotNull(), appView));
+    Value out = createValue(TypeElement.fromDexType(type, definitelyNotNull(), appView));
     return new ConstClass(out, type);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 7f8d5d3..03ca81d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.ListIterator;
@@ -38,6 +39,11 @@
   }
 
   @Override
+  public Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value) {
+    return instructionIterator.insertConstStringInstruction(appView, code, value);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     instructionIterator.replaceCurrentInstructionWithConstInt(code, value);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index e46af2c..c562fe7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -250,6 +250,16 @@
     return get(Opcodes.SUB);
   }
 
+  @SuppressWarnings("ConstantConditions")
+  public boolean mayHaveSwitch() {
+    assert Opcodes.INT_SWITCH < 64;
+    assert Opcodes.STRING_SWITCH < 64;
+    long mask = (1L << Opcodes.INT_SWITCH) | (1L << Opcodes.STRING_SWITCH);
+    boolean result = isAnySetInFirst(mask);
+    assert result == (mayHaveIntSwitch() || mayHaveStringSwitch());
+    return result;
+  }
+
   public boolean mayHaveUshr() {
     return get(Opcodes.USHR);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 623da4d..371438e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
@@ -64,6 +65,8 @@
 
   Value insertConstIntInstruction(IRCode code, InternalOptions options, int value);
 
+  Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value);
+
   void replaceCurrentInstructionWithConstInt(IRCode code, int value);
 
   void replaceCurrentInstructionWithStaticGet(
diff --git a/src/main/java/com/android/tools/r8/ir/code/IntSwitch.java b/src/main/java/com/android/tools/r8/ir/code/IntSwitch.java
index f22ef1c..e0e3cdc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IntSwitch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IntSwitch.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.code.SparseSwitch;
 import com.android.tools.r8.code.SparseSwitchPayload;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
@@ -51,13 +52,18 @@
   }
 
   @Override
+  public Instruction materializeFirstKey(AppView<?> appView, IRCode code) {
+    return code.createIntConstant(getFirstKey());
+  }
+
+  @Override
   public boolean valid() {
     assert super.valid();
     assert keys.length >= 1;
     assert keys.length <= Constants.U16BIT_MAX;
     // Keys must be acceding, and cannot target the fallthrough.
     assert keys.length == numberOfKeys();
-    for (int i = 1; i < keys.length - 1; i++) {
+    for (int i = 1; i < keys.length; i++) {
       assert keys[i - 1] < keys[i];
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 2a82bcc..15700bf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.ListIterator;
@@ -51,6 +52,11 @@
   }
 
   @Override
+  public Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value) {
+    return currentBlockIterator.insertConstStringInstruction(appView, code, value);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
     currentBlockIterator.replaceCurrentInstructionWithConstInt(code, value);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 49779aa..26e808f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -170,7 +170,7 @@
             + "that is not defined on all control-flow paths leading to the use.");
   }
 
-  private void appendOperand(Value operand) {
+  public void appendOperand(Value operand) {
     operands.add(operand);
     operand.addPhiUser(this);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
index 1b3d673..e25685c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StringSwitch.java
@@ -7,10 +7,12 @@
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import java.util.function.BiConsumer;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
+import com.android.tools.r8.utils.ThrowingConsumer;
 
 public class StringSwitch extends Switch {
 
@@ -23,6 +25,10 @@
     assert valid();
   }
 
+  public DexString getFirstKey() {
+    return keys[0];
+  }
+
   @Override
   public int opcode() {
     return Opcodes.STRING_SWITCH;
@@ -33,13 +39,25 @@
     return visitor.visit(this);
   }
 
-  public void forEachCase(BiConsumer<DexString, BasicBlock> fn) {
+  public <E extends Throwable> void forEachKey(ThrowingConsumer<DexString, E> fn) throws E {
+    for (DexString key : keys) {
+      fn.accept(key);
+    }
+  }
+
+  public <E extends Throwable> void forEachCase(ThrowingBiConsumer<DexString, BasicBlock, E> fn)
+      throws E {
     for (int i = 0; i < keys.length; i++) {
       fn.accept(getKey(i), targetBlock(i));
     }
   }
 
   @Override
+  public Instruction materializeFirstKey(AppView<?> appView, IRCode code) {
+    return code.createStringConstant(appView, getFirstKey());
+  }
+
+  @Override
   public boolean valid() {
     assert super.valid();
     assert keys.length >= 1;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Switch.java b/src/main/java/com/android/tools/r8/ir/code/Switch.java
index 8dcb6a4..4e129aa 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Switch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Switch.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.graph.AppView;
 import java.util.function.Consumer;
 
 public abstract class Switch extends JumpInstruction {
@@ -17,6 +18,8 @@
     this.fallthroughBlockIndex = fallthroughBlockIndex;
   }
 
+  public abstract Instruction materializeFirstKey(AppView<?> appView, IRCode code);
+
   public Value value() {
     return inValues.get(0);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 6550178..ae2f9d7 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -1340,11 +1340,6 @@
     timing.begin("Propogate sparse conditionals");
     new SparseConditionalConstantPropagation(code).run();
     timing.end();
-    if (stringSwitchRemover != null) {
-      timing.begin("Remove string switch");
-      stringSwitchRemover.run(method, code);
-      timing.end();
-    }
     timing.begin("Rewrite always throwing invokes");
     codeRewriter.processMethodsNeverReturningNormally(code);
     timing.end();
@@ -1514,6 +1509,14 @@
 
     previous = printMethod(code, "IR after outline handler (SSA)", previous);
 
+    if (stringSwitchRemover != null) {
+      // Remove string switches prior to canonicalization to ensure that the constants that are
+      // being introduced will be canonicalized if possible.
+      timing.begin("Remove string switch");
+      stringSwitchRemover.run(method, code);
+      timing.end();
+    }
+
     // TODO(mkroghj) Test if shorten live ranges is worth it.
     if (!options.isGeneratingClassFiles()) {
       timing.begin("Canonicalize constants");
@@ -1649,6 +1652,9 @@
 
   public void removeDeadCodeAndFinalizeIR(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
+    if (stringSwitchRemover != null) {
+      stringSwitchRemover.run(method, code);
+    }
     deadCodeRemover.run(code, timing);
     finalizeIR(method, code, feedback, timing);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
index 0365d43..50bec23 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.conversion;
 
+import static com.android.tools.r8.ir.code.ConstNumber.asConstNumberOrNull;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
@@ -24,11 +26,12 @@
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.io.UTFDataFormatException;
 import java.util.ArrayList;
-import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -231,7 +234,7 @@
           return null;
         }
 
-        Map<DexString, BasicBlock> stringToTargetMapping = new IdentityHashMap<>();
+        Map<DexString, BasicBlock> stringToTargetMapping = new LinkedHashMap<>();
         for (DexString key : stringToIdMapping.mapping.keySet()) {
           int id = stringToIdMapping.mapping.getInt(key);
           BasicBlock target = idToTargetMapping.mapping.get(id);
@@ -378,7 +381,7 @@
             InvokeVirtual invoke = instruction.asInvokeVirtual();
             if (invoke.getInvokedMethod() == dexItemFactory.stringMembers.hashCode
                 && invoke.getReceiver() == stringValue
-                && invoke.outValue().onlyUsedInBlock(block)) {
+                && (!invoke.hasOutValue() || invoke.outValue().onlyUsedInBlock(block))) {
               continue;
             }
           }
@@ -430,7 +433,7 @@
         }
 
         // Go into the true-target and record a mapping from all strings to their id.
-        Reference2IntMap<DexString> extension = new Reference2IntOpenHashMap<>();
+        Reference2IntMap<DexString> extension = new Reference2IntLinkedOpenHashMap<>();
         BasicBlock ifEqualsHashTarget = Utils.getTrueTarget(theIf);
         if (!addMappingsForStringsWithHash(ifEqualsHashTarget, hash, extension)) {
           // Not a valid extension of `toBeExtended`.
@@ -460,7 +463,7 @@
         }
 
         // Go into each switch case and record a mapping from all strings to their id.
-        Reference2IntMap<DexString> extension = new Reference2IntOpenHashMap<>();
+        Reference2IntMap<DexString> extension = new Reference2IntLinkedOpenHashMap<>();
         for (int i = 0; i < theSwitch.numberOfKeys(); i++) {
           int hash = theSwitch.getKey(i);
           BasicBlock equalsHashTarget = theSwitch.targetBlock(i);
@@ -494,18 +497,36 @@
           Set<BasicBlock> visited) {
         InstructionIterator instructionIterator = block.iterator();
 
-        // Verify that the first instruction is a non-throwing const-string instruction.
-        // If the string throws, it can't be decoded, and then the string does not have a hash.
-        ConstString theString = instructionIterator.next().asConstString();
-        if (theString == null || theString.instructionInstanceCanThrow()) {
+        // The first instruction is expected to be a non-throwing const-string instruction, but this
+        // may change due to canonicalization. If the string throws, it can't be decoded, and then
+        // the string does not have a hash.
+        Instruction first = instructionIterator.next();
+        ConstString optionalString = first.asConstString();
+        if (optionalString != null && optionalString.instructionInstanceCanThrow()) {
           return false;
         }
 
-        InvokeVirtual theInvoke = instructionIterator.next().asInvokeVirtual();
+        // The next instruction must be an invoke-virtual that calls stringValue.equals() with a
+        // constant string argument.
+        InvokeVirtual theInvoke =
+            first.isConstString()
+                ? instructionIterator.next().asInvokeVirtual()
+                : first.asInvokeVirtual();
         if (theInvoke == null
             || theInvoke.getInvokedMethod() != dexItemFactory.stringMembers.equals
-            || theInvoke.getReceiver() != stringValue
-            || theInvoke.inValues().get(1) != theString.outValue()) {
+            || theInvoke.getReceiver() != stringValue) {
+          return false;
+        }
+
+        // If this block starts with a const-string instruction, then it should be passed as the
+        // second argument to equals().
+        if (optionalString != null && theInvoke.getArgument(1) != optionalString.outValue()) {
+          assert false; // This should generally not happen.
+          return false;
+        }
+
+        Value theString = theInvoke.getArgument(1).getAliasedValue();
+        if (!theString.isDefinedByInstructionSatisfying(Instruction::isConstString)) {
           return false;
         }
 
@@ -518,9 +539,10 @@
         }
 
         try {
-          if (theString.getValue().decodedHashCode() == hash) {
-            BasicBlock trueTarget = theIf.targetFromCondition(1).endOfGotoChain();
-            if (!addMappingForString(trueTarget, theString.getValue(), extension)) {
+          DexString theStringValue = theString.definition.asConstString().getValue();
+          if (theStringValue.decodedHashCode() == hash) {
+            BasicBlock trueTarget = theIf.targetFromCondition(1);
+            if (!addMappingForString(trueTarget, theStringValue, extension)) {
               return false;
             }
           }
@@ -545,7 +567,17 @@
       private boolean addMappingForString(
           BasicBlock block, DexString string, Reference2IntMap<DexString> extension) {
         InstructionIterator instructionIterator = block.iterator();
-        ConstNumber constNumberInstruction = instructionIterator.next().asConstNumber();
+        ConstNumber constNumberInstruction;
+        if (block.isTrivialGoto()) {
+          if (block.getUniqueNormalSuccessor() != idValue.getBlock()) {
+            return false;
+          }
+          int predecessorIndex = idValue.getBlock().getPredecessors().indexOf(block);
+          constNumberInstruction =
+              asConstNumberOrNull(idValue.getOperand(predecessorIndex).definition);
+        } else {
+          constNumberInstruction = instructionIterator.next().asConstNumber();
+        }
         if (constNumberInstruction == null
             || !idValue.getOperands().contains(constNumberInstruction.outValue())) {
           return false;
@@ -568,7 +600,7 @@
     // The hash value of interest.
     private final Value stringHashValue;
 
-    private final Reference2IntMap<DexString> mapping = new Reference2IntOpenHashMap<>();
+    private final Reference2IntMap<DexString> mapping = new Reference2IntLinkedOpenHashMap<>();
 
     private StringToIdMapping(Value stringHashValue, DexItemFactory dexItemFactory) {
       assert isDefinedByStringHashCode(stringHashValue, dexItemFactory);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchRemover.java
index fcfc1e9..ebce25c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchRemover.java
@@ -4,11 +4,14 @@
 
 package com.android.tools.r8.ir.conversion;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.naming.IdentifierNameStringUtils.isClassNameValue;
+
+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.DexString;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
-import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -17,17 +20,27 @@
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.InvokeVirtual;
-import com.android.tools.r8.ir.code.JumpInstruction;
+import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StringSwitch;
+import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.naming.IdentifierNameStringMarker;
+import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
-import java.util.IdentityHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceRBTreeMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.io.UTFDataFormatException;
+import java.util.LinkedHashMap;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -43,7 +56,7 @@
   StringSwitchRemover(AppView<?> appView, IdentifierNameStringMarker identifierNameStringMarker) {
     this.appView = appView;
     this.identifierNameStringMarker = identifierNameStringMarker;
-    this.stringType = TypeElement.stringClassType(appView, Nullability.definitelyNotNull());
+    this.stringType = TypeElement.stringClassType(appView, definitelyNotNull());
     this.throwingInfo = ThrowingInfo.defaultForConstString(appView.options());
   }
 
@@ -53,106 +66,426 @@
       return;
     }
 
-    Set<BasicBlock> newBlocks = Sets.newIdentityHashSet();
+    if (!prepareForStringSwitchRemoval(code)) {
+      return;
+    }
+
+    Set<BasicBlock> newBlocksWithStrings = Sets.newIdentityHashSet();
 
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
-      JumpInstruction exit = block.exit();
-      if (exit.isStringSwitch()) {
-        removeStringSwitch(code, blockIterator, block, exit.asStringSwitch(), newBlocks);
+      StringSwitch theSwitch = block.exit().asStringSwitch();
+      if (theSwitch != null) {
+        try {
+          SingleStringSwitchRemover remover;
+          if (theSwitch.numberOfKeys() < appView.options().minimumStringSwitchSize
+              || hashCodeOfKeysMayChangeAfterMinification(theSwitch)) {
+            remover =
+                new SingleEqualityBasedStringSwitchRemover(
+                    code, blockIterator, block, theSwitch, newBlocksWithStrings);
+          } else {
+            remover =
+                new SingleHashBasedStringSwitchRemover(
+                    code, blockIterator, block, theSwitch, newBlocksWithStrings);
+          }
+          remover.removeStringSwitch();
+        } catch (UTFDataFormatException e) {
+          // The keys of a string-switch should never fail to decode.
+          throw new Unreachable();
+        }
       }
     }
 
     if (identifierNameStringMarker != null) {
-      identifierNameStringMarker.decoupleIdentifierNameStringsInBlocks(method, code, newBlocks);
+      identifierNameStringMarker.decoupleIdentifierNameStringsInBlocks(
+          method, code, newBlocksWithStrings);
     }
 
     assert code.isConsistentSSA();
   }
 
-  private void removeStringSwitch(
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      BasicBlock block,
-      StringSwitch theSwitch,
-      Set<BasicBlock> newBlocks) {
-    int nextBlockNumber = code.getHighestBlockNumber() + 1;
+  // Returns true if minification is enabled and the switch value is guaranteed to be a class name.
+  // In this case, we can't use the hash codes of the keys before minification, because they will
+  // (potentially) change as a result of minification. Therefore, we currently emit a sequence of
+  // if-equals checks for such switches.
+  //
+  // TODO(b/154483187): This should also use the hash-based string switch elimination.
+  private boolean hashCodeOfKeysMayChangeAfterMinification(StringSwitch theSwitch) {
+    return appView.options().isMinifying()
+        && isClassNameValue(theSwitch.value(), appView.dexItemFactory());
+  }
 
-    BasicBlock fallthroughBlock = theSwitch.fallthroughBlock();
-    Map<DexString, BasicBlock> stringToTargetMap = new IdentityHashMap<>();
-    theSwitch.forEachCase(stringToTargetMap::put);
+  private boolean prepareForStringSwitchRemoval(IRCode code) {
+    boolean hasStringSwitch = false;
+    ListIterator<BasicBlock> blockIterator = code.listIterator();
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      for (BasicBlock predecessor : block.getNormalPredecessors()) {
+        StringSwitch exit = predecessor.exit().asStringSwitch();
+        if (exit != null) {
+          hasStringSwitch = true;
+          if (block == exit.fallthroughBlock()) {
+            // After the elimination of this string-switch instruction, there will be two
+            // fallthrough blocks: one for the instruction that switches on the hash value and one
+            // for the instruction that switches on the string id value.
+            //
+            // The existing fallthrough block will be the fallthrough block for the switch on the
+            // hash value. Note that we can't use this block for the switch on the id value since
+            // that would lead to critical edges.
+            BasicBlock hashSwitchFallthroughBlock = block;
 
-    // Remove outgoing control flow edges from the block containing the string switch.
-    for (BasicBlock successor : block.getNormalSuccessors()) {
-      successor.removePredecessor(block, null);
-    }
-    block.removeAllNormalSuccessors();
+            // The `hashSwitchFallthroughBlock` will jump to the switch on the string id value.
+            // This block will have multiple predecessors, hence the need for the split-edge
+            // block.
+            BasicBlock idSwitchBlock =
+                hashSwitchFallthroughBlock.listIterator(code).split(code, blockIterator);
 
-    Set<BasicBlock> blocksTargetedByMultipleSwitchCases = Sets.newIdentityHashSet();
-    {
-      Set<BasicBlock> seenBefore = SetUtils.newIdentityHashSet(stringToTargetMap.size());
-      for (BasicBlock targetBlock : stringToTargetMap.values()) {
-        if (!seenBefore.add(targetBlock)) {
-          blocksTargetedByMultipleSwitchCases.add(targetBlock);
+            // Split again such that `idSwitchBlock` becomes a block consisting of a single goto
+            // instruction that targets a block that is identical to the original fallthrough
+            // block of the string-switch instruction.
+            BasicBlock idSwitchFallthroughBlock =
+                idSwitchBlock.listIterator(code).split(code, blockIterator);
+            break;
+          }
         }
       }
     }
 
-    // Create a String.equals() check for each case in the string-switch instruction.
-    BasicBlock previous = null;
-    for (Entry<DexString, BasicBlock> entry : stringToTargetMap.entrySet()) {
-      ConstString constStringInstruction =
-          new ConstString(code.createValue(stringType), entry.getKey(), throwingInfo);
-      constStringInstruction.setPosition(Position.syntheticNone());
+    return hasStringSwitch;
+  }
 
-      InvokeVirtual invokeInstruction =
-          new InvokeVirtual(
-              appView.dexItemFactory().stringMembers.equals,
-              code.createValue(PrimitiveTypeElement.getInt()),
-              ImmutableList.of(theSwitch.value(), constStringInstruction.outValue()));
-      invokeInstruction.setPosition(Position.syntheticNone());
+  private abstract static class SingleStringSwitchRemover {
 
-      If ifInstruction = new If(If.Type.NE, invokeInstruction.outValue());
-      ifInstruction.setPosition(Position.none());
+    final IRCode code;
+    final ListIterator<BasicBlock> blockIterator;
+    final Set<BasicBlock> newBlocksWithStrings;
 
-      BasicBlock targetBlock = entry.getValue();
-      if (blocksTargetedByMultipleSwitchCases.contains(targetBlock)) {
-        // Need an intermediate block to avoid critical edges.
-        BasicBlock intermediateBlock =
-            BasicBlock.createGotoBlock(nextBlockNumber++, Position.none(), code.metadata());
-        intermediateBlock.link(targetBlock);
-        blockIterator.add(intermediateBlock);
-        newBlocks.add(intermediateBlock);
-        targetBlock = intermediateBlock;
-      }
+    final Position position;
+    final Value stringValue;
 
-      BasicBlock newBlock =
-          BasicBlock.createIfBlock(
-              nextBlockNumber++,
-              ifInstruction,
-              code.metadata(),
-              constStringInstruction,
-              invokeInstruction);
-      newBlock.link(targetBlock);
-      blockIterator.add(newBlock);
-      newBlocks.add(newBlock);
-
-      if (previous == null) {
-        // Replace the string-switch instruction by a goto instruction.
-        block.exit().replace(new Goto(newBlock), code);
-        block.link(newBlock);
-      } else {
-        // Set the fallthrough block for the previously added if-instruction.
-        previous.link(newBlock);
-      }
-
-      previous = newBlock;
+    private SingleStringSwitchRemover(
+        IRCode code,
+        ListIterator<BasicBlock> blockIterator,
+        StringSwitch theSwitch,
+        Set<BasicBlock> newBlocksWithStrings) {
+      this.code = code;
+      this.blockIterator = blockIterator;
+      this.newBlocksWithStrings = newBlocksWithStrings;
+      this.position = theSwitch.getPosition();
+      this.stringValue = theSwitch.value();
     }
 
-    assert previous != null;
+    abstract void removeStringSwitch();
+  }
 
-    // Set the fallthrough block for the last if-instruction.
-    previous.link(fallthroughBlock);
+  private class SingleEqualityBasedStringSwitchRemover extends SingleStringSwitchRemover {
+
+    private final BasicBlock block;
+    private final BasicBlock fallthroughBlock;
+
+    private final Map<DexString, BasicBlock> structure;
+
+    private SingleEqualityBasedStringSwitchRemover(
+        IRCode code,
+        ListIterator<BasicBlock> blockIterator,
+        BasicBlock block,
+        StringSwitch theSwitch,
+        Set<BasicBlock> newBlocksWithStrings) {
+      super(code, blockIterator, theSwitch, newBlocksWithStrings);
+      this.block = block;
+      this.fallthroughBlock = theSwitch.fallthroughBlock();
+      this.structure = createStructure(theSwitch);
+    }
+
+    private Map<DexString, BasicBlock> createStructure(StringSwitch theSwitch) {
+      Map<DexString, BasicBlock> result = new LinkedHashMap<>();
+      theSwitch.forEachCase(result::put);
+      return result;
+    }
+
+    @Override
+    void removeStringSwitch() {
+      int nextBlockNumber = code.getHighestBlockNumber() + 1;
+      // Remove outgoing control flow edges from the block containing the string switch.
+      for (BasicBlock successor : block.getNormalSuccessors()) {
+        successor.removePredecessor(block, null);
+      }
+      block.removeAllNormalSuccessors();
+      Set<BasicBlock> blocksTargetedByMultipleSwitchCases = Sets.newIdentityHashSet();
+      {
+        Set<BasicBlock> seenBefore = SetUtils.newIdentityHashSet(structure.size());
+        for (BasicBlock targetBlock : structure.values()) {
+          if (!seenBefore.add(targetBlock)) {
+            blocksTargetedByMultipleSwitchCases.add(targetBlock);
+          }
+        }
+      }
+      // Create a String.equals() check for each case in the string-switch instruction.
+      BasicBlock previous = null;
+      for (Entry<DexString, BasicBlock> entry : structure.entrySet()) {
+        ConstString constStringInstruction =
+            new ConstString(code.createValue(stringType), entry.getKey(), throwingInfo);
+        constStringInstruction.setPosition(position);
+        InvokeVirtual invokeInstruction =
+            new InvokeVirtual(
+                appView.dexItemFactory().stringMembers.equals,
+                code.createValue(PrimitiveTypeElement.getInt()),
+                ImmutableList.of(stringValue, constStringInstruction.outValue()));
+        invokeInstruction.setPosition(position);
+        If ifInstruction = new If(If.Type.NE, invokeInstruction.outValue());
+        ifInstruction.setPosition(Position.none());
+        BasicBlock targetBlock = entry.getValue();
+        if (blocksTargetedByMultipleSwitchCases.contains(targetBlock)) {
+          // Need an intermediate block to avoid critical edges.
+          BasicBlock intermediateBlock =
+              BasicBlock.createGotoBlock(nextBlockNumber++, Position.none(), code.metadata());
+          intermediateBlock.link(targetBlock);
+          blockIterator.add(intermediateBlock);
+          newBlocksWithStrings.add(intermediateBlock);
+          targetBlock = intermediateBlock;
+        }
+        BasicBlock newBlock =
+            BasicBlock.createIfBlock(
+                nextBlockNumber++,
+                ifInstruction,
+                code.metadata(),
+                constStringInstruction,
+                invokeInstruction);
+        newBlock.link(targetBlock);
+        blockIterator.add(newBlock);
+        newBlocksWithStrings.add(newBlock);
+        if (previous == null) {
+          // Replace the string-switch instruction by a goto instruction.
+          block.exit().replace(new Goto(newBlock), code);
+          block.link(newBlock);
+        } else {
+          // Set the fallthrough block for the previously added if-instruction.
+          previous.link(newBlock);
+        }
+        previous = newBlock;
+      }
+      assert previous != null;
+      // Set the fallthrough block for the last if-instruction.
+      previous.link(fallthroughBlock);
+    }
+  }
+
+  private class SingleHashBasedStringSwitchRemover extends SingleStringSwitchRemover {
+
+    private final BasicBlock hashSwitchBlock;
+    private final BasicBlock hashSwitchFallthroughBlock;
+    private final BasicBlock idSwitchBlock;
+    private final BasicBlock idSwitchFallthroughBlock;
+
+    Int2ReferenceMap<Map<DexString, BasicBlock>> structure;
+
+    private int nextBlockNumber;
+    private int nextStringId;
+
+    private SingleHashBasedStringSwitchRemover(
+        IRCode code,
+        ListIterator<BasicBlock> blockIterator,
+        BasicBlock hashSwitchBlock,
+        StringSwitch theSwitch,
+        Set<BasicBlock> newBlocksWithStrings)
+        throws UTFDataFormatException {
+      super(code, blockIterator, theSwitch, newBlocksWithStrings);
+      this.hashSwitchBlock = hashSwitchBlock;
+      this.hashSwitchFallthroughBlock = theSwitch.fallthroughBlock();
+      this.idSwitchBlock = theSwitch.fallthroughBlock().getUniqueNormalSuccessor();
+      this.idSwitchFallthroughBlock = idSwitchBlock.getUniqueNormalSuccessor();
+      this.structure = createStructure(theSwitch);
+      this.nextBlockNumber = code.getHighestBlockNumber() + 1;
+    }
+
+    private int getAndIncrementNextBlockNumber() {
+      return nextBlockNumber++;
+    }
+
+    private Int2ReferenceMap<Map<DexString, BasicBlock>> createStructure(StringSwitch theSwitch)
+        throws UTFDataFormatException {
+      Int2ReferenceMap<Map<DexString, BasicBlock>> result = new Int2ReferenceRBTreeMap<>();
+      theSwitch.forEachCase(
+          (key, target) -> {
+            int hashCode = key.decodedHashCode();
+            if (result.containsKey(hashCode)) {
+              result.get(hashCode).put(key, target);
+            } else {
+              Map<DexString, BasicBlock> cases = new LinkedHashMap<>();
+              cases.put(key, target);
+              result.put(hashCode, cases);
+            }
+          });
+      return result;
+    }
+
+    @Override
+    void removeStringSwitch() {
+      // Remove outgoing control flow edges from the block containing the string switch.
+      for (BasicBlock successor : hashSwitchBlock.getNormalSuccessors()) {
+        successor.removePredecessor(hashSwitchBlock, null);
+      }
+      hashSwitchBlock.removeAllNormalSuccessors();
+
+      // 1. Insert `int id = -1`.
+      InstructionListIterator instructionIterator =
+          hashSwitchBlock.listIterator(code, hashSwitchBlock.size());
+      instructionIterator.previous();
+
+      Phi idPhi = code.createPhi(idSwitchBlock, TypeElement.getInt());
+      Value notFoundIdValue =
+          instructionIterator.insertConstIntInstruction(code, appView.options(), -1);
+      idPhi.appendOperand(notFoundIdValue);
+
+      // 2. Insert `int hashCode = stringValue.hashCode()`.
+      InvokeVirtual hashInvoke =
+          new InvokeVirtual(
+              appView.dexItemFactory().stringMembers.hashCode,
+              code.createValue(TypeElement.getInt()),
+              ImmutableList.of(stringValue));
+      hashInvoke.setPosition(position);
+      instructionIterator.add(hashInvoke);
+
+      // 3. Create all the target blocks of the hash switch.
+      //
+      // Say that the string switch instruction contains the keys "X" and "Y" and assume that "X"
+      // and "Y" have the same hash code. Then this will create code that looks like:
+      //
+      //   Block N:
+      //     boolean equalsX = stringValue.equals("X")
+      //     if equalsX then goto block N+1 else goto block N+2
+      //   Block N+1:
+      //     id = 0
+      //     goto <id-switch-block>
+      //   Block N+2:
+      //     boolean equalsY = stringValue.equals("Y")
+      //     if equalsY then goto block N+3 else goto block N+4
+      //   Block N+3:
+      //     id = 1
+      //     goto <id-switch-block>
+      //   Block N+4:
+      //     goto <id-switch-block>
+      createHashSwitchTargets(idPhi, notFoundIdValue);
+      hashSwitchBlock.link(hashSwitchFallthroughBlock);
+
+      // 4. Insert `switch (hashValue)`.
+      IntSwitch hashSwitch = createHashSwitch(hashInvoke.outValue());
+      instructionIterator.next();
+      instructionIterator.replaceCurrentInstruction(hashSwitch);
+
+      // 5. Link `idSwitchBlock` with all of its target blocks.
+      Reference2IntMap<BasicBlock> targetBlockIndices = new Reference2IntOpenHashMap<>();
+      targetBlockIndices.defaultReturnValue(-1);
+      idSwitchBlock.getMutableSuccessors().clear();
+      for (Map<DexString, BasicBlock> cases : structure.values()) {
+        for (BasicBlock target : cases.values()) {
+          int targetIndex = targetBlockIndices.getInt(target);
+          if (targetIndex == -1) {
+            targetBlockIndices.put(target, idSwitchBlock.getSuccessors().size());
+            idSwitchBlock.link(target);
+          }
+        }
+      }
+      idSwitchBlock.getMutableSuccessors().add(idSwitchFallthroughBlock);
+
+      // 6. Insert `switch (idValue)`.
+      IntSwitch idSwitch = createIdSwitch(idPhi, targetBlockIndices);
+      InstructionListIterator idSwitchBlockInstructionIterator = idSwitchBlock.listIterator(code);
+      idSwitchBlockInstructionIterator.next();
+      idSwitchBlockInstructionIterator.replaceCurrentInstruction(idSwitch);
+    }
+
+    private IntSwitch createHashSwitch(Value hashValue) {
+      int[] hashSwitchKeys = structure.keySet().toArray(new int[0]);
+      int[] hashSwitchTargetIndices = new int[hashSwitchKeys.length];
+      for (int i = 0, offset = hashSwitchBlock.numberOfExceptionalSuccessors();
+          i < hashSwitchTargetIndices.length;
+          i++) {
+        hashSwitchTargetIndices[i] = i + offset;
+      }
+      int hashSwitchFallthroughIndex = hashSwitchBlock.getSuccessors().size() - 1;
+      return new IntSwitch(
+          hashValue, hashSwitchKeys, hashSwitchTargetIndices, hashSwitchFallthroughIndex);
+    }
+
+    private void createHashSwitchTargets(Phi idPhi, Value notFoundIdValue) {
+      for (Map<DexString, BasicBlock> cases : structure.values()) {
+        // Create the target block for the hash switch.
+        BasicBlock hashBlock =
+            BasicBlock.createGotoBlock(getAndIncrementNextBlockNumber(), position, code.metadata());
+        blockIterator.add(hashBlock);
+        hashSwitchBlock.link(hashBlock);
+
+        // Position the block iterator at the newly created block.
+        BasicBlock previous = blockIterator.previous();
+        assert previous == hashBlock;
+        blockIterator.next();
+
+        BasicBlock current = hashBlock;
+        for (Entry<DexString, BasicBlock> entry : cases.entrySet()) {
+          current.getMutableSuccessors().clear();
+
+          // Insert `String key = <entry.getKey()>`.
+          InstructionListIterator instructionIterator = current.listIterator(code);
+          Value keyValue =
+              instructionIterator.insertConstStringInstruction(appView, code, entry.getKey());
+          newBlocksWithStrings.add(current);
+
+          // Insert `boolean equalsKey = stringValue.equals(key)`.
+          InvokeVirtual equalsInvoke =
+              new InvokeVirtual(
+                  appView.dexItemFactory().stringMembers.equals,
+                  code.createValue(TypeElement.getInt()),
+                  ImmutableList.of(stringValue, keyValue));
+          equalsInvoke.setPosition(position);
+          instructionIterator.add(equalsInvoke);
+
+          // Create a new block for the success case.
+          BasicBlock equalsKeyBlock =
+              BasicBlock.createGotoBlock(
+                  getAndIncrementNextBlockNumber(), position, code.metadata(), idSwitchBlock);
+          idSwitchBlock.getMutablePredecessors().add(equalsKeyBlock);
+          blockIterator.add(equalsKeyBlock);
+          current.link(equalsKeyBlock);
+
+          // Insert `int id = <nextStringId++>`.
+          Value idValue =
+              equalsKeyBlock
+                  .listIterator(code)
+                  .insertConstIntInstruction(code, appView.options(), nextStringId++);
+          idPhi.appendOperand(idValue);
+
+          // Create a new block for the failure case.
+          BasicBlock continuationBlock =
+              BasicBlock.createGotoBlock(
+                  getAndIncrementNextBlockNumber(), position, code.metadata(), idSwitchBlock);
+          blockIterator.add(continuationBlock);
+          current.link(continuationBlock);
+
+          // Insert `if (equalsKey) goto <id-switch-block> else goto <continuation-block>`.
+          instructionIterator.next();
+          instructionIterator.replaceCurrentInstruction(new If(Type.NE, equalsInvoke.outValue()));
+
+          current = continuationBlock;
+        }
+        idPhi.appendOperand(notFoundIdValue);
+        idSwitchBlock.getMutablePredecessors().add(current);
+      }
+    }
+
+    private IntSwitch createIdSwitch(Phi idPhi, Reference2IntMap<BasicBlock> targetBlockIndices) {
+      int numberOfCases = nextStringId;
+      int[] keys = ArrayUtils.createIdentityArray(numberOfCases);
+      int[] targetIndices = new int[numberOfCases];
+      int i = 0;
+      for (Map<DexString, BasicBlock> cases : structure.values()) {
+        for (Entry<DexString, BasicBlock> entry : cases.entrySet()) {
+          targetIndices[i++] = targetBlockIndices.getInt(entry.getValue());
+        }
+      }
+      int fallthroughIndex = targetBlockIndices.size();
+      return new IntSwitch(idPhi, keys, targetIndices, fallthroughIndex);
+    }
   }
 }
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 11180c4..12e4c5e 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
@@ -368,7 +368,8 @@
     DexType vivifiedType =
         appView
             .dexItemFactory()
-            .createType(DescriptorUtils.javaTypeToDescriptor(VIVIFIED_PREFIX + type.toString()));
+            .createSynthesizedType(
+                DescriptorUtils.javaTypeToDescriptor(VIVIFIED_PREFIX + type.toString()));
     appView.rewritePrefix.rewriteType(vivifiedType, type);
     return vivifiedType;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
index 0e52152..64825c1 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
@@ -484,6 +484,6 @@
             + '$'
             + suffix
             + ';';
-    return appView.dexItemFactory().createType(descriptor);
+    return appView.dexItemFactory().createSynthesizedType(descriptor);
   }
 }
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 28ac0fd..9bdc9b0 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
@@ -709,7 +709,7 @@
     assert type.isClassType();
     String descriptor = type.descriptor.toString();
     String elTypeDescriptor = getEmulateLibraryInterfaceClassDescriptor(descriptor);
-    return factory.createType(elTypeDescriptor);
+    return factory.createSynthesizedType(elTypeDescriptor);
   }
 
   private void reportStaticInterfaceMethodHandle(DexMethod referencedFrom, DexMethodHandle handle) {
@@ -737,7 +737,7 @@
     assert type.isClassType();
     String descriptor = type.descriptor.toString();
     String ccTypeDescriptor = getCompanionClassDescriptor(descriptor);
-    return factory.createType(ccTypeDescriptor);
+    return factory.createSynthesizedType(ccTypeDescriptor);
   }
 
   DexType getCompanionClassType(DexType type) {
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 9f34126..eb36e39 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
@@ -948,7 +948,7 @@
   }
 
   public boolean rewriteSwitch(IRCode code, SwitchCaseAnalyzer switchCaseAnalyzer) {
-    if (!code.metadata().mayHaveIntSwitch()) {
+    if (!code.metadata().mayHaveSwitch()) {
       return false;
     }
 
@@ -959,8 +959,8 @@
       InstructionListIterator iterator = block.listIterator(code);
       while (iterator.hasNext()) {
         Instruction instruction = iterator.next();
-        if (instruction.isIntSwitch()) {
-          IntSwitch theSwitch = instruction.asIntSwitch();
+        if (instruction.isSwitch()) {
+          Switch theSwitch = instruction.asSwitch();
           if (options.testing.enableDeadSwitchCaseElimination) {
             SwitchCaseEliminator eliminator =
                 removeUnnecessarySwitchCases(code, theSwitch, iterator, switchCaseAnalyzer);
@@ -975,119 +975,12 @@
                 continue;
               }
 
-              assert instruction.isIntSwitch();
-              theSwitch = instruction.asIntSwitch();
+              assert instruction.isSwitch();
+              theSwitch = instruction.asSwitch();
             }
           }
-          if (theSwitch.numberOfKeys() == 1) {
-            // Rewrite the switch to an if.
-            int fallthroughBlockIndex = theSwitch.getFallthroughBlockIndex();
-            int caseBlockIndex = theSwitch.targetBlockIndices()[0];
-            if (fallthroughBlockIndex < caseBlockIndex) {
-              block.swapSuccessorsByIndex(fallthroughBlockIndex, caseBlockIndex);
-            }
-            if (theSwitch.getFirstKey() == 0) {
-              iterator.replaceCurrentInstruction(new If(Type.EQ, theSwitch.value()));
-            } else {
-              ConstNumber labelConst = code.createIntConstant(theSwitch.getFirstKey());
-              labelConst.setPosition(theSwitch.getPosition());
-              iterator.previous();
-              iterator.add(labelConst);
-              Instruction dummy = iterator.next();
-              assert dummy == theSwitch;
-              If theIf = new If(Type.EQ, ImmutableList.of(theSwitch.value(), labelConst.dest()));
-              iterator.replaceCurrentInstruction(theIf);
-            }
-          } else {
-            // If there are more than 1 key, we use the following algorithm to find keys to combine.
-            // First, scan through the keys forward and combine each packed interval with the
-            // previous interval if it gives a net saving.
-            // Secondly, go through all created intervals and combine the ones without a saving into
-            // a single interval and keep a max number of packed switches.
-            // Finally, go through all intervals and check if the switch or part of the switch
-            // should be transformed to ifs.
-
-            // Phase 1: Combine packed intervals.
-            InternalOutputMode mode = options.getInternalOutputMode();
-            int[] keys = theSwitch.getKeys();
-            int maxNumberOfIfsOrSwitches = 10;
-            PriorityQueue<Interval> biggestPackedSavings =
-                new PriorityQueue<>(
-                    (x, y) -> Long.compare(y.packedSavings(mode), x.packedSavings(mode)));
-            Set<Interval> biggestPackedSet = new HashSet<>();
-            List<Interval> intervals = new ArrayList<>();
-            int previousKey = keys[0];
-            IntList currentKeys = new IntArrayList();
-            currentKeys.add(previousKey);
-            Interval previousInterval = null;
-            for (int i = 1; i < keys.length; i++) {
-              int key = keys[i];
-              if (((long) key - (long) previousKey) > 1) {
-                Interval current = new Interval(currentKeys);
-                Interval added = combineOrAddInterval(intervals, previousInterval, current);
-                if (added != current && biggestPackedSet.contains(previousInterval)) {
-                  biggestPackedSet.remove(previousInterval);
-                  biggestPackedSavings.remove(previousInterval);
-                }
-                tryAddToBiggestSavings(
-                    biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches);
-                previousInterval = added;
-                currentKeys = new IntArrayList();
-              }
-              currentKeys.add(key);
-              previousKey = key;
-            }
-            Interval current = new Interval(currentKeys);
-            Interval added = combineOrAddInterval(intervals, previousInterval, current);
-            if (added != current && biggestPackedSet.contains(previousInterval)) {
-              biggestPackedSet.remove(previousInterval);
-              biggestPackedSavings.remove(previousInterval);
-            }
-            tryAddToBiggestSavings(
-                biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches);
-
-            // Phase 2: combine sparse intervals into a single bin.
-            // Check if we should save a space for a sparse switch, if so, remove the switch with
-            // the smallest savings.
-            if (biggestPackedSet.size() == maxNumberOfIfsOrSwitches
-                && maxNumberOfIfsOrSwitches < intervals.size()) {
-              biggestPackedSet.remove(biggestPackedSavings.poll());
-            }
-            Interval sparse = null;
-            List<Interval> newSwitches = new ArrayList<>(maxNumberOfIfsOrSwitches);
-            for (int i = 0; i < intervals.size(); i++) {
-              Interval interval = intervals.get(i);
-              if (biggestPackedSet.contains(interval)) {
-                newSwitches.add(interval);
-              } else if (sparse == null) {
-                sparse = interval;
-                newSwitches.add(sparse);
-              } else {
-                sparse.addInterval(interval);
-              }
-            }
-
-            // Phase 3: at this point we are guaranteed to have the biggest saving switches
-            // in newIntervals, potentially with a switch combining the remaining intervals.
-            // Now we check to see if we can create any if's to reduce size.
-            IntList outliers = new IntArrayList();
-            int outliersAsIfSize =
-                appView.options().testing.enableSwitchToIfRewriting
-                    ? findIfsForCandidates(newSwitches, theSwitch, outliers)
-                    : 0;
-
-            long newSwitchesSize = 0;
-            List<IntList> newSwitchSequences = new ArrayList<>(newSwitches.size());
-            for (Interval interval : newSwitches) {
-              newSwitchesSize += interval.estimatedSize(mode);
-              newSwitchSequences.add(interval.keys);
-            }
-
-            long currentSize = IntSwitch.estimatedSize(mode, theSwitch.getKeys());
-            if (newSwitchesSize + outliersAsIfSize + codeUnitMargin() < currentSize) {
-              convertSwitchToSwitchAndIfs(
-                  code, blocksIterator, block, iterator, theSwitch, newSwitchSequences, outliers);
-            }
+          if (theSwitch.isIntSwitch()) {
+            rewriteIntSwitch(code, blocksIterator, block, iterator, theSwitch.asIntSwitch());
           }
         }
       }
@@ -1107,9 +1000,127 @@
     return !affectedValues.isEmpty();
   }
 
+  private void rewriteIntSwitch(
+      IRCode code,
+      ListIterator<BasicBlock> blockIterator,
+      BasicBlock block,
+      InstructionListIterator iterator,
+      IntSwitch theSwitch) {
+    if (theSwitch.numberOfKeys() == 1) {
+      // Rewrite the switch to an if.
+      int fallthroughBlockIndex = theSwitch.getFallthroughBlockIndex();
+      int caseBlockIndex = theSwitch.targetBlockIndices()[0];
+      if (fallthroughBlockIndex < caseBlockIndex) {
+        block.swapSuccessorsByIndex(fallthroughBlockIndex, caseBlockIndex);
+      }
+      If replacement;
+      if (theSwitch.isIntSwitch() && theSwitch.asIntSwitch().getFirstKey() == 0) {
+        replacement = new If(Type.EQ, theSwitch.value());
+      } else {
+        Instruction labelConst = theSwitch.materializeFirstKey(appView, code);
+        labelConst.setPosition(theSwitch.getPosition());
+        iterator.previous();
+        iterator.add(labelConst);
+        Instruction dummy = iterator.next();
+        assert dummy == theSwitch;
+        replacement = new If(Type.EQ, ImmutableList.of(theSwitch.value(), labelConst.outValue()));
+      }
+      iterator.replaceCurrentInstruction(replacement);
+      return;
+    }
+
+    // If there are more than 1 key, we use the following algorithm to find keys to combine.
+    // First, scan through the keys forward and combine each packed interval with the
+    // previous interval if it gives a net saving.
+    // Secondly, go through all created intervals and combine the ones without a saving into
+    // a single interval and keep a max number of packed switches.
+    // Finally, go through all intervals and check if the switch or part of the switch
+    // should be transformed to ifs.
+
+    // Phase 1: Combine packed intervals.
+    InternalOutputMode mode = options.getInternalOutputMode();
+    int[] keys = theSwitch.getKeys();
+    int maxNumberOfIfsOrSwitches = 10;
+    PriorityQueue<Interval> biggestPackedSavings =
+        new PriorityQueue<>((x, y) -> Long.compare(y.packedSavings(mode), x.packedSavings(mode)));
+    Set<Interval> biggestPackedSet = new HashSet<>();
+    List<Interval> intervals = new ArrayList<>();
+    int previousKey = keys[0];
+    IntList currentKeys = new IntArrayList();
+    currentKeys.add(previousKey);
+    Interval previousInterval = null;
+    for (int i = 1; i < keys.length; i++) {
+      int key = keys[i];
+      if (((long) key - (long) previousKey) > 1) {
+        Interval current = new Interval(currentKeys);
+        Interval added = combineOrAddInterval(intervals, previousInterval, current);
+        if (added != current && biggestPackedSet.contains(previousInterval)) {
+          biggestPackedSet.remove(previousInterval);
+          biggestPackedSavings.remove(previousInterval);
+        }
+        tryAddToBiggestSavings(
+            biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches);
+        previousInterval = added;
+        currentKeys = new IntArrayList();
+      }
+      currentKeys.add(key);
+      previousKey = key;
+    }
+    Interval current = new Interval(currentKeys);
+    Interval added = combineOrAddInterval(intervals, previousInterval, current);
+    if (added != current && biggestPackedSet.contains(previousInterval)) {
+      biggestPackedSet.remove(previousInterval);
+      biggestPackedSavings.remove(previousInterval);
+    }
+    tryAddToBiggestSavings(biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches);
+
+    // Phase 2: combine sparse intervals into a single bin.
+    // Check if we should save a space for a sparse switch, if so, remove the switch with
+    // the smallest savings.
+    if (biggestPackedSet.size() == maxNumberOfIfsOrSwitches
+        && maxNumberOfIfsOrSwitches < intervals.size()) {
+      biggestPackedSet.remove(biggestPackedSavings.poll());
+    }
+    Interval sparse = null;
+    List<Interval> newSwitches = new ArrayList<>(maxNumberOfIfsOrSwitches);
+    for (int i = 0; i < intervals.size(); i++) {
+      Interval interval = intervals.get(i);
+      if (biggestPackedSet.contains(interval)) {
+        newSwitches.add(interval);
+      } else if (sparse == null) {
+        sparse = interval;
+        newSwitches.add(sparse);
+      } else {
+        sparse.addInterval(interval);
+      }
+    }
+
+    // Phase 3: at this point we are guaranteed to have the biggest saving switches
+    // in newIntervals, potentially with a switch combining the remaining intervals.
+    // Now we check to see if we can create any if's to reduce size.
+    IntList outliers = new IntArrayList();
+    int outliersAsIfSize =
+        appView.options().testing.enableSwitchToIfRewriting
+            ? findIfsForCandidates(newSwitches, theSwitch, outliers)
+            : 0;
+
+    long newSwitchesSize = 0;
+    List<IntList> newSwitchSequences = new ArrayList<>(newSwitches.size());
+    for (Interval interval : newSwitches) {
+      newSwitchesSize += interval.estimatedSize(mode);
+      newSwitchSequences.add(interval.keys);
+    }
+
+    long currentSize = IntSwitch.estimatedSize(mode, theSwitch.getKeys());
+    if (newSwitchesSize + outliersAsIfSize + codeUnitMargin() < currentSize) {
+      convertSwitchToSwitchAndIfs(
+          code, blockIterator, block, iterator, theSwitch, newSwitchSequences, outliers);
+    }
+  }
+
   private SwitchCaseEliminator removeUnnecessarySwitchCases(
       IRCode code,
-      IntSwitch theSwitch,
+      Switch theSwitch,
       InstructionListIterator iterator,
       SwitchCaseAnalyzer switchCaseAnalyzer) {
     BasicBlock defaultTarget = theSwitch.fallthroughBlock();
@@ -1118,9 +1129,18 @@
         new BasicBlockBehavioralSubsumption(appView, code.method.holder());
 
     // Compute the set of switch cases that can be removed.
+    int alwaysHitCase = -1;
     for (int i = 0; i < theSwitch.numberOfKeys(); i++) {
       BasicBlock targetBlock = theSwitch.targetBlock(i);
 
+      if (switchCaseAnalyzer.switchCaseIsAlwaysHit(theSwitch, i)) {
+        if (eliminator == null) {
+          eliminator = new SwitchCaseEliminator(theSwitch, iterator);
+        }
+        eliminator.markSwitchCaseAsAlwaysHit(i);
+        break;
+      }
+
       // This switch case can be removed if the behavior of the target block is equivalent to the
       // behavior of the default block, or if the switch case is unreachable.
       if (switchCaseAnalyzer.switchCaseIsUnreachable(theSwitch, i)
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
index eef169e..64a8c86 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ReflectionOptimizer.java
@@ -189,7 +189,7 @@
     }
     // Make sure the (base) type is resolvable.
     DexType baseType = type.toBaseType(dexItemFactory);
-    DexClass baseClazz = appView.definitionFor(baseType);
+    DexClass baseClazz = appView.appInfo().definitionForWithoutExistenceAssert(baseType);
     if (baseClazz == null || !baseClazz.isResolvable(appView)) {
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index d626168..3285e2a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -36,6 +36,7 @@
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * ServiceLoaderRewriter will attempt to rewrite calls on the form of: ServiceLoader.load(X.class,
@@ -69,7 +70,7 @@
   public static final String SERVICE_LOADER_CLASS_NAME = "$$ServiceLoaderMethods";
   private static final String SERVICE_LOADER_METHOD_PREFIX_NAME = "$load";
 
-  private DexProgramClass synthesizedClass;
+  private AtomicReference<DexProgramClass> synthesizedClass = new AtomicReference<>();
   private ConcurrentHashMap<DexType, DexEncodedMethod> synthesizedServiceLoaders =
       new ConcurrentHashMap<>();
 
@@ -82,7 +83,7 @@
   }
 
   public DexProgramClass getSynthesizedClass() {
-    return synthesizedClass;
+    return synthesizedClass.get();
   }
 
   public void rewrite(IRCode code) {
@@ -184,40 +185,12 @@
   }
 
   private DexEncodedMethod createSynthesizedMethod(DexType serviceType, List<DexClass> classes) {
-    DexType serviceLoaderType =
-        appView.dexItemFactory().createType("L" + SERVICE_LOADER_CLASS_NAME + ";");
-    if (synthesizedClass == null) {
-      assert !appView.options().encodeChecksums;
-      ChecksumSupplier checksumSupplier = DexProgramClass::invalidChecksumRequest;
-      synthesizedClass =
-          new DexProgramClass(
-              serviceLoaderType,
-              null,
-              new SynthesizedOrigin("Service Loader desugaring", getClass()),
-              ClassAccessFlags.fromDexAccessFlags(
-                  Constants.ACC_FINAL | Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC),
-              appView.dexItemFactory().objectType,
-              DexTypeList.empty(),
-              appView.dexItemFactory().createString("ServiceLoader"),
-              null,
-              Collections.emptyList(),
-              null,
-              Collections.emptyList(),
-              DexAnnotationSet.empty(),
-              DexEncodedField.EMPTY_ARRAY, // Static fields.
-              DexEncodedField.EMPTY_ARRAY, // Instance fields.
-              DexEncodedMethod.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY, // Virtual methods.
-              appView.dexItemFactory().getSkipNameValidationForTesting(),
-              checksumSupplier);
-      appView.appInfo().addSynthesizedClass(synthesizedClass);
-    }
     DexProto proto = appView.dexItemFactory().createProto(appView.dexItemFactory().iteratorType);
     DexMethod method =
         appView
             .dexItemFactory()
             .createMethod(
-                serviceLoaderType,
+                appView.dexItemFactory().serviceLoaderRewrittenClassType,
                 proto,
                 SERVICE_LOADER_METHOD_PREFIX_NAME + atomicInteger.incrementAndGet());
     MethodAccessFlags methodAccess =
@@ -230,10 +203,48 @@
             ParameterAnnotationsList.empty(),
             ServiceLoaderSourceCode.generate(serviceType, classes, appView.dexItemFactory()),
             true);
-    synthesizedClass.addDirectMethod(encodedMethod);
+    getOrSetSynthesizedClass().addDirectMethod(encodedMethod);
     return encodedMethod;
   }
 
+  private DexProgramClass getOrSetSynthesizedClass() {
+    if (synthesizedClass.get() != null) {
+      return synthesizedClass.get();
+    }
+    assert !appView.options().encodeChecksums;
+    ChecksumSupplier checksumSupplier = DexProgramClass::invalidChecksumRequest;
+    DexProgramClass clazz =
+        synthesizedClass.updateAndGet(
+            existingClazz -> {
+              if (existingClazz != null) {
+                return existingClazz;
+              }
+              return new DexProgramClass(
+                  appView.dexItemFactory().serviceLoaderRewrittenClassType,
+                  null,
+                  new SynthesizedOrigin("Service Loader desugaring", getClass()),
+                  ClassAccessFlags.fromDexAccessFlags(
+                      Constants.ACC_FINAL | Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC),
+                  appView.dexItemFactory().objectType,
+                  DexTypeList.empty(),
+                  appView.dexItemFactory().createString("ServiceLoader"),
+                  null,
+                  Collections.emptyList(),
+                  null,
+                  Collections.emptyList(),
+                  DexAnnotationSet.empty(),
+                  DexEncodedField.EMPTY_ARRAY, // Static fields.
+                  DexEncodedField.EMPTY_ARRAY, // Instance fields.
+                  DexEncodedMethod.EMPTY_ARRAY,
+                  DexEncodedMethod.EMPTY_ARRAY, // Virtual methods.
+                  appView.dexItemFactory().getSkipNameValidationForTesting(),
+                  checksumSupplier);
+            });
+    assert clazz != null;
+    appView.appInfo().addSynthesizedClass(clazz);
+    return clazz;
+  }
+
   /**
    * Rewriter assumes that the code is of the form:
    *
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java b/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java
index 0aabe6e..733db77 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
+import com.android.tools.r8.ir.code.StringSwitch;
+import com.android.tools.r8.ir.code.Switch;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
@@ -21,12 +24,14 @@
   private final BasicBlock block;
   private final BasicBlock defaultTarget;
   private final InstructionListIterator iterator;
-  private final IntSwitch theSwitch;
+  private final Switch theSwitch;
 
+  private int alwaysHitCase = -1;
+  private BasicBlock alwaysHitTarget;
   private boolean mayHaveIntroducedUnreachableBlocks = false;
   private IntSet switchCasesToBeRemoved;
 
-  SwitchCaseEliminator(IntSwitch theSwitch, InstructionListIterator iterator) {
+  SwitchCaseEliminator(Switch theSwitch, InstructionListIterator iterator) {
     this.block = theSwitch.getBlock();
     this.defaultTarget = theSwitch.fallthroughBlock();
     this.iterator = iterator;
@@ -40,13 +45,34 @@
 
   private boolean canBeOptimized() {
     assert switchCasesToBeRemoved == null || !switchCasesToBeRemoved.isEmpty();
-    return switchCasesToBeRemoved != null;
+    return switchCasesToBeRemoved != null || hasAlwaysHitCase();
   }
 
   boolean mayHaveIntroducedUnreachableBlocks() {
     return mayHaveIntroducedUnreachableBlocks;
   }
 
+  public boolean isSwitchCaseLive(int index) {
+    if (hasAlwaysHitCase()) {
+      return index == alwaysHitCase;
+    }
+    return !switchCasesToBeRemoved.contains(index);
+  }
+
+  public boolean isFallthroughLive() {
+    return !hasAlwaysHitCase();
+  }
+
+  public boolean hasAlwaysHitCase() {
+    return alwaysHitCase >= 0;
+  }
+
+  void markSwitchCaseAsAlwaysHit(int i) {
+    assert alwaysHitCase < 0;
+    alwaysHitCase = i;
+    alwaysHitTarget = theSwitch.targetBlock(i);
+  }
+
   void markSwitchCaseForRemoval(int i) {
     if (switchCasesToBeRemoved == null) {
       switchCasesToBeRemoved = new IntOpenHashSet();
@@ -58,8 +84,8 @@
     if (canBeOptimized()) {
       int originalNumberOfSuccessors = block.getSuccessors().size();
       unlinkDeadSuccessors();
-      if (allSwitchCasesMarkedForRemoval()) {
-        // Replace switch with a simple goto since only the fall through is left.
+      if (hasAlwaysHitCase() || allSwitchCasesMarkedForRemoval()) {
+        // Replace switch with a simple goto.
         replaceSwitchByGoto();
       } else {
         // Replace switch by a new switch where the dead switch cases have been removed.
@@ -90,12 +116,14 @@
   private IntPredicate computeSuccessorHasBecomeDeadPredicate() {
     int[] numberOfControlFlowEdgesToBlockWithIndex = new int[block.getSuccessors().size()];
     for (int i = 0; i < theSwitch.numberOfKeys(); i++) {
-      if (!switchCasesToBeRemoved.contains(i)) {
+      if (isSwitchCaseLive(i)) {
         int targetBlockIndex = theSwitch.getTargetBlockIndex(i);
         numberOfControlFlowEdgesToBlockWithIndex[targetBlockIndex] += 1;
       }
     }
-    numberOfControlFlowEdgesToBlockWithIndex[theSwitch.getFallthroughBlockIndex()] += 1;
+    if (isFallthroughLive()) {
+      numberOfControlFlowEdgesToBlockWithIndex[theSwitch.getFallthroughBlockIndex()] += 1;
+    }
     for (int i : block.getCatchHandlersWithSuccessorIndexes().getUniqueTargets()) {
       numberOfControlFlowEdgesToBlockWithIndex[i] += 1;
     }
@@ -103,7 +131,9 @@
   }
 
   private void replaceSwitchByGoto() {
-    iterator.replaceCurrentInstruction(new Goto(defaultTarget));
+    assert !hasAlwaysHitCase() || alwaysHitTarget != null;
+    BasicBlock target = hasAlwaysHitCase() ? alwaysHitTarget : defaultTarget;
+    iterator.replaceCurrentInstruction(new Goto(target));
   }
 
   private void replaceSwitchByOptimizedSwitch(int originalNumberOfSuccessors) {
@@ -121,11 +151,9 @@
     }
 
     int newNumberOfKeys = theSwitch.numberOfKeys() - switchCasesToBeRemoved.size();
-    int[] newKeys = new int[newNumberOfKeys];
     int[] newTargetBlockIndices = new int[newNumberOfKeys];
     for (int i = 0, j = 0; i < theSwitch.numberOfKeys(); i++) {
       if (!switchCasesToBeRemoved.contains(i)) {
-        newKeys[j] = theSwitch.getKey(i);
         newTargetBlockIndices[j] =
             theSwitch.getTargetBlockIndex(i)
                 - targetBlockIndexOffset[theSwitch.getTargetBlockIndex(i)];
@@ -135,12 +163,41 @@
       }
     }
 
-    iterator.replaceCurrentInstruction(
-        new IntSwitch(
-            theSwitch.value(),
-            newKeys,
-            newTargetBlockIndices,
-            theSwitch.getFallthroughBlockIndex()
-                - targetBlockIndexOffset[theSwitch.getFallthroughBlockIndex()]));
+    Switch replacement;
+    if (theSwitch.isIntSwitch()) {
+      IntSwitch intSwitch = theSwitch.asIntSwitch();
+      int[] newKeys = new int[newNumberOfKeys];
+      for (int i = 0, j = 0; i < theSwitch.numberOfKeys(); i++) {
+        if (!switchCasesToBeRemoved.contains(i)) {
+          newKeys[j] = intSwitch.getKey(i);
+          j++;
+        }
+      }
+      replacement =
+          new IntSwitch(
+              theSwitch.value(),
+              newKeys,
+              newTargetBlockIndices,
+              theSwitch.getFallthroughBlockIndex()
+                  - targetBlockIndexOffset[theSwitch.getFallthroughBlockIndex()]);
+    } else {
+      assert theSwitch.isStringSwitch();
+      StringSwitch stringSwitch = theSwitch.asStringSwitch();
+      DexString[] newKeys = new DexString[newNumberOfKeys];
+      for (int i = 0, j = 0; i < theSwitch.numberOfKeys(); i++) {
+        if (!switchCasesToBeRemoved.contains(i)) {
+          newKeys[j] = stringSwitch.getKey(i);
+          j++;
+        }
+      }
+      replacement =
+          new StringSwitch(
+              theSwitch.value(),
+              newKeys,
+              newTargetBlockIndices,
+              theSwitch.getFallthroughBlockIndex()
+                  - targetBlockIndexOffset[theSwitch.getFallthroughBlockIndex()]);
+    }
+    iterator.replaceCurrentInstruction(replacement);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/controlflow/SwitchCaseAnalyzer.java b/src/main/java/com/android/tools/r8/ir/optimize/controlflow/SwitchCaseAnalyzer.java
index 6c22a72..76f63ac 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/controlflow/SwitchCaseAnalyzer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/controlflow/SwitchCaseAnalyzer.java
@@ -4,8 +4,11 @@
 
 package com.android.tools.r8.ir.optimize.controlflow;
 
-import com.android.tools.r8.ir.code.IntSwitch;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Switch;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.LongInterval;
 
 public class SwitchCaseAnalyzer {
 
@@ -17,9 +20,35 @@
     return INSTANCE;
   }
 
-  public boolean switchCaseIsUnreachable(IntSwitch theSwitch, int index) {
+  public boolean switchCaseIsAlwaysHit(Switch theSwitch, int index) {
     Value switchValue = theSwitch.value();
-    return switchValue.hasValueRange()
-        && !switchValue.getValueRange().containsValue(theSwitch.getKey(index));
+    if (theSwitch.isIntSwitch()) {
+      LongInterval valueRange = switchValue.getValueRange();
+      return valueRange != null
+          && valueRange.isSingleValue()
+          && valueRange.containsValue(theSwitch.asIntSwitch().getKey(index));
+    }
+
+    assert theSwitch.isStringSwitch();
+
+    Value rootSwitchValue = switchValue.getAliasedValue();
+    DexString key = theSwitch.asStringSwitch().getKey(index);
+    return rootSwitchValue.isDefinedByInstructionSatisfying(Instruction::isConstString)
+        && key == rootSwitchValue.definition.asConstString().getValue();
+  }
+
+  public boolean switchCaseIsUnreachable(Switch theSwitch, int index) {
+    Value switchValue = theSwitch.value();
+    if (theSwitch.isIntSwitch()) {
+      return switchValue.hasValueRange()
+          && !switchValue.getValueRange().containsValue(theSwitch.asIntSwitch().getKey(index));
+    }
+
+    assert theSwitch.isStringSwitch();
+
+    Value rootSwitchValue = switchValue.getAliasedValue();
+    DexString key = theSwitch.asStringSwitch().getKey(index);
+    return rootSwitchValue.isDefinedByInstructionSatisfying(Instruction::isConstString)
+        && key != rootSwitchValue.definition.asConstString().getValue();
   }
 }
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 0a35a6f..61a7c13 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
@@ -138,6 +138,14 @@
           if (outValue.getType().isNullType()) {
             addNullDependencies(outValue.uniqueUsers(), eligibleEnums);
           }
+        } else {
+          if (instruction.isInvokeMethod()) {
+            DexProgramClass enumClass =
+                getEnumUnboxingCandidateOrNull(instruction.asInvokeMethod().getReturnType());
+            if (enumClass != null) {
+              eligibleEnums.add(enumClass.type);
+            }
+          }
         }
         if (instruction.isConstClass()) {
           analyzeConstClass(instruction.asConstClass(), eligibleEnums);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index 0b39506..1bda5d7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -151,8 +151,9 @@
   }
 
   private void removePinnedIfNotHolder(DexMember<?, ?> member, DexType type) {
-    if (type != member.holder) {
-      removePinnedCandidate(type);
+    DexType baseType = type.toBaseType(factory);
+    if (baseType != member.holder) {
+      removePinnedCandidate(baseType);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java
index 386a32d..953514d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java
@@ -16,8 +16,7 @@
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import java.util.LinkedHashMap;
 
 /**
  * Extracts the ordinal values and any anonymous subtypes for all Enum classes from their static
@@ -57,12 +56,8 @@
     }
     DexEncodedMethod initializer = clazz.getClassInitializer();
     IRCode code = initializer.getCode().buildIR(initializer, appView, clazz.origin);
-    Map<DexField, EnumValueInfo> enumValueInfoMap = new IdentityHashMap<>();
-    for (Instruction insn : code.instructions()) {
-      if (!insn.isStaticPut()) {
-        continue;
-      }
-      StaticPut staticPut = insn.asStaticPut();
+    LinkedHashMap<DexField, EnumValueInfo> enumValueInfoMap = new LinkedHashMap<>();
+    for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
       if (staticPut.getField().type != clazz.type) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index 30766c5..97d0029 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfo;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
+import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.code.ArrayGet;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlock.ThrowingInfo;
@@ -26,15 +27,21 @@
 import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
-import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.Arrays;
+import java.util.Set;
 
 public class EnumValueOptimizer {
 
@@ -157,36 +164,92 @@
    * </blockquote>
    */
   public void removeSwitchMaps(IRCode code) {
+    Set<Value> affectedValues = Sets.newIdentityHashSet();
+    boolean mayHaveIntroducedUnreachableBlocks = false;
     for (BasicBlock block : code.blocks) {
-      JumpInstruction exit = block.exit();
+      IntSwitch switchInsn = block.exit().asIntSwitch();
       // Pattern match a switch on a switch map as input.
-      if (!exit.isIntSwitch()) {
+      if (switchInsn == null) {
         continue;
       }
-      IntSwitch switchInsn = exit.asIntSwitch();
+
       EnumSwitchInfo info = analyzeSwitchOverEnum(switchInsn);
       if (info == null) {
         continue;
       }
-      Int2IntMap targetMap = new Int2IntArrayMap();
+
+      Int2IntMap ordinalToTargetMap = new Int2IntArrayMap(switchInsn.numberOfKeys());
       for (int i = 0; i < switchInsn.numberOfKeys(); i++) {
         assert switchInsn.targetBlockIndices()[i] != switchInsn.getFallthroughBlockIndex();
         DexField field = info.indexMap.get(switchInsn.getKey(i));
         EnumValueInfo valueInfo = info.valueInfoMap.getEnumValueInfo(field);
-        targetMap.put(valueInfo.ordinal, switchInsn.targetBlockIndices()[i]);
+        if (valueInfo != null) {
+          ordinalToTargetMap.put(valueInfo.ordinal, switchInsn.targetBlockIndices()[i]);
+        } else {
+          // The switch map refers to a field on the enum that does not exist in this compilation.
+        }
       }
-      int[] keys = targetMap.keySet().toIntArray();
+
+      int fallthroughBlockIndex = switchInsn.getFallthroughBlockIndex();
+      if (ordinalToTargetMap.size() < switchInsn.numberOfKeys()) {
+        // There is at least one dead switch case. This can happen when some dependencies use
+        // different versions of the same enum.
+        int numberOfNormalSuccessors = switchInsn.numberOfKeys() + 1;
+        int numberOfExceptionalSuccessors = block.numberOfExceptionalSuccessors();
+        IntSet ordinalToTargetValues = new IntOpenHashSet(ordinalToTargetMap.values());
+
+        // Compute which successors that are dead. We don't include the exceptional successors,
+        // since none of them are dead. Therefore, `deadBlockIndices[i]` represents if the i'th
+        // normal successor is dead, i.e., if the (i+numberOfExceptionalSuccessors)'th successor is
+        // dead.
+        //
+        // Note: we use an int[] to efficiently fixup `ordinalToTargetMap` below.
+        int[] deadBlockIndices =
+            ArrayUtils.fromPredicate(
+                index -> {
+                  // It is dead if it is not targeted by a switch case and it is not the fallthrough
+                  // block.
+                  int adjustedIndex = index + numberOfExceptionalSuccessors;
+                  return !ordinalToTargetValues.contains(adjustedIndex)
+                      && adjustedIndex != switchInsn.getFallthroughBlockIndex();
+                },
+                numberOfNormalSuccessors);
+
+        // Detach the dead successors from the graph, and record that we need to remove unreachable
+        // blocks in the end.
+        IntList successorIndicesToRemove = new IntArrayList(numberOfNormalSuccessors);
+        for (int i = 0; i < numberOfNormalSuccessors; i++) {
+          if (deadBlockIndices[i] == 1) {
+            BasicBlock successor = block.getSuccessors().get(i + numberOfExceptionalSuccessors);
+            successor.removePredecessor(block, affectedValues);
+            successorIndicesToRemove.add(i);
+          }
+        }
+        block.removeSuccessorsByIndex(successorIndicesToRemove);
+        mayHaveIntroducedUnreachableBlocks = true;
+
+        // Fixup `ordinalToTargetMap` and the fallthrough index.
+        ArrayUtils.sumOfPredecessorsInclusive(deadBlockIndices);
+        for (Int2IntMap.Entry entry : ordinalToTargetMap.int2IntEntrySet()) {
+          ordinalToTargetMap.put(
+              entry.getIntKey(), entry.getIntValue() - deadBlockIndices[entry.getIntValue()]);
+        }
+        fallthroughBlockIndex -= deadBlockIndices[fallthroughBlockIndex];
+      }
+
+      int[] keys = ordinalToTargetMap.keySet().toIntArray();
       Arrays.sort(keys);
       int[] targets = new int[keys.length];
       for (int i = 0; i < keys.length; i++) {
-        targets[i] = targetMap.get(keys[i]);
+        targets[i] = ordinalToTargetMap.get(keys[i]);
       }
 
       IntSwitch newSwitch =
-          new IntSwitch(
-              info.ordinalInvoke.outValue(), keys, targets, switchInsn.getFallthroughBlockIndex());
+          new IntSwitch(info.ordinalInvoke.outValue(), keys, targets, fallthroughBlockIndex);
+
       // Replace the switch itself.
-      exit.replace(newSwitch, code);
+      switchInsn.replace(newSwitch, code);
+
       // If the original input to the switch is now unused, remove it too. It is not dead
       // as it might have side-effects but we ignore these here.
       Instruction arrayGet = info.arrayGet;
@@ -194,12 +257,19 @@
         arrayGet.inValues().forEach(v -> v.removeUser(arrayGet));
         arrayGet.getBlock().removeInstruction(arrayGet);
       }
+
       Instruction staticGet = info.staticGet;
       if (!staticGet.outValue().hasUsers()) {
         assert staticGet.inValues().isEmpty();
         staticGet.getBlock().removeInstruction(staticGet);
       }
     }
+    if (mayHaveIntroducedUnreachableBlocks) {
+      affectedValues.addAll(code.removeUnreachableBlocks());
+    }
+    if (!affectedValues.isEmpty()) {
+      new TypeAnalysis(appView).narrowing(affectedValues);
+    }
   }
 
   private static final class EnumSwitchInfo {
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 d1ddce9..da135b0 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
@@ -271,7 +271,7 @@
             DexMethod invokedMethod = invoke.getInvokedMethod();
             DexType returnType = invokedMethod.proto.returnType;
             if (returnType.isClassType()
-                && appView.appInfo().isRelatedBySubtyping(returnType, method.holder())) {
+                && appView.appInfo().inSameHierarchy(returnType, method.holder())) {
               return; // Not allowed, could introduce an alias of the receiver.
             }
             callsReceiver.add(new Pair<>(Invoke.Type.VIRTUAL, invokedMethod));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
index 2683d2f..c718391 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/BridgeInfo.java
@@ -9,6 +9,8 @@
  */
 public abstract class BridgeInfo {
 
+  public abstract boolean hasSameTarget(BridgeInfo bridgeInfo);
+
   public boolean isVirtualBridgeInfo() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
index a07abdc..585699f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/bridge/VirtualBridgeInfo.java
@@ -38,6 +38,13 @@
   }
 
   @Override
+  public boolean hasSameTarget(BridgeInfo bridgeInfo) {
+    assert bridgeInfo.isVirtualBridgeInfo();
+    VirtualBridgeInfo virtualBridgeInfo = bridgeInfo.asVirtualBridgeInfo();
+    return invokedMethod.match(virtualBridgeInfo.invokedMethod);
+  }
+
+  @Override
   public boolean isVirtualBridgeInfo() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
index bed05c0..2779111 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataSynthesizer.java
@@ -170,6 +170,9 @@
           originalTypeInfo != null && originalTypeInfo.isObjectArray()
               ? originalTypeInfo.getArguments().get(0).variance
               : KmVariance.INVARIANT;
+      if (variance == null) {
+        variance = KmVariance.INVARIANT;
+      }
       renamedKmType.getArguments().add(new KmTypeProjection(variance, argumentType));
     }
     return renamedKmType;
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
index e9bc069..75d80b1 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinTypeInfo.java
@@ -70,7 +70,7 @@
   }
 
   public boolean isObjectArray() {
-    if (!isClass()) {
+    if (isClass()) {
       KmClassifier.Class classifier = (KmClassifier.Class) this.classifier;
       return classifier.getName().equals(ClassClassifiers.arrayBinaryName) && arguments.size() == 1;
     }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
index b2f5a80..ba73522 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringUtils.java
@@ -178,12 +178,13 @@
             || isClassNameValue(invoke.inValues().get(1), dexItemFactory));
   }
 
-  private static boolean isClassNameValue(Value value, DexItemFactory dexItemFactory) {
+  public static boolean isClassNameValue(Value value, DexItemFactory dexItemFactory) {
     Value root = value.getAliasedValue();
-    return !root.isPhi()
-        && root.definition.isInvokeVirtual()
-        && root.definition.asInvokeVirtual().getInvokedMethod()
-            == dexItemFactory.classMethods.getName;
+    if (!root.isDefinedByInstructionSatisfying(Instruction::isInvokeVirtual)) {
+      return false;
+    }
+    InvokeVirtual invoke = root.definition.asInvokeVirtual();
+    return dexItemFactory.classMethods.isReflectiveNameLookup(invoke.getInvokedMethod());
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java
new file mode 100644
index 0000000..d3ca402
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeHoisting.java
@@ -0,0 +1,268 @@
+// 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.optimize;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
+import com.android.tools.r8.ir.optimize.info.bridge.VirtualBridgeInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * An optimization pass that hoists bridges upwards with the purpose of sharing redundant bridge
+ * methods.
+ *
+ * <p>Example: <code>
+ *   class A {
+ *     void m() { ... }
+ *   }
+ *   class B1 extends A {
+ *     void bridge() { m(); }
+ *   }
+ *   class B2 extends A {
+ *     void bridge() { m(); }
+ *   }
+ * </code> Is transformed into: <code>
+ *   class A {
+ *     void m() { ... }
+ *     void bridge() { m(); }
+ *   }
+ *   class B1 extends A {}
+ *   class B2 extends A {}
+ * </code>
+ */
+public class BridgeHoisting {
+
+  private final AppView<AppInfoWithLiveness> appView;
+
+  // A lens that keeps track of the changes for construction of the Proguard map.
+  private final BridgeHoistingLens.Builder lensBuilder = new BridgeHoistingLens.Builder();
+
+  public BridgeHoisting(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public void run() {
+    BottomUpClassHierarchyTraversal.forProgramClasses(appView)
+        .excludeInterfaces()
+        .visit(appView.appInfo().classes(), this::processClass);
+    if (!lensBuilder.isEmpty()) {
+      BridgeHoistingLens lens = lensBuilder.build(appView);
+      boolean changed = appView.setGraphLense(lens);
+      assert changed;
+      appView.setAppInfo(
+          appView.appInfo().rewrittenWithLens(appView.appInfo().app().asDirect(), lens));
+    }
+  }
+
+  private void processClass(DexProgramClass clazz) {
+    Set<DexType> subtypes = appView.appInfo().allImmediateSubtypes(clazz.type);
+    Set<DexProgramClass> subclasses = new TreeSet<>((x, y) -> x.type.slowCompareTo(y.type));
+    for (DexType subtype : subtypes) {
+      DexProgramClass subclass = asProgramClassOrNull(appView.definitionFor(subtype));
+      if (subclass == null) {
+        return;
+      }
+      subclasses.add(subclass);
+    }
+    for (Wrapper<DexMethod> candidate : getCandidatesForHoisting(subclasses)) {
+      hoistBridgeIfPossible(candidate.get(), clazz, subclasses);
+    }
+  }
+
+  private Set<Wrapper<DexMethod>> getCandidatesForHoisting(Set<DexProgramClass> subclasses) {
+    Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+    Set<Wrapper<DexMethod>> candidates = new HashSet<>();
+    for (DexProgramClass subclass : subclasses) {
+      for (DexEncodedMethod method : subclass.virtualMethods()) {
+        BridgeInfo bridgeInfo = method.getOptimizationInfo().getBridgeInfo();
+        // TODO(b/153147967): Even if the bridge is not targeting a method in the superclass, it may
+        //  be possible to rewrite the bridge to target a method in the superclass, such that we can
+        //  hoist it. Add a test.
+        if (bridgeInfo != null && bridgeIsTargetingMethodInSuperclass(subclass, bridgeInfo)) {
+          candidates.add(equivalence.wrap(method.method));
+        }
+      }
+    }
+    return candidates;
+  }
+
+  /**
+   * Returns true if the bridge method is referencing a method in the superclass of {@param holder}.
+   * If this is not the case, we cannot hoist the bridge method, as that would lead to a type error:
+   * <code>
+   *   class A {
+   *     void bridge() {
+   *       v0 <- Argument
+   *       invoke-virtual {v0}, void B.m() // <- not valid
+   *       Return
+   *     }
+   *   }
+   *   class B extends A {
+   *     void m() {
+   *       ...
+   *     }
+   *   }
+   * </code>
+   */
+  private boolean bridgeIsTargetingMethodInSuperclass(
+      DexProgramClass holder, BridgeInfo bridgeInfo) {
+    if (bridgeInfo.isVirtualBridgeInfo()) {
+      VirtualBridgeInfo virtualBridgeInfo = bridgeInfo.asVirtualBridgeInfo();
+      DexMethod invokedMethod = virtualBridgeInfo.getInvokedMethod();
+      assert !appView.appInfo().isStrictSubtypeOf(invokedMethod.holder, holder.type);
+      if (invokedMethod.holder == holder.type) {
+        return false;
+      }
+      assert appView.appInfo().isStrictSubtypeOf(holder.type, invokedMethod.holder);
+      return true;
+    }
+    assert false;
+    return false;
+  }
+
+  private void hoistBridgeIfPossible(
+      DexMethod method, DexProgramClass clazz, Set<DexProgramClass> subclasses) {
+    // If the method is defined on the parent class, we cannot hoist the bridge.
+    // TODO(b/153147967): If the declared method is abstract, we could replace it by the bridge.
+    //  Add a test.
+    if (clazz.lookupMethod(method) != null) {
+      return;
+    }
+
+    // Go through each of the subclasses and bail-out if each subclass does not declare the same
+    // bridge.
+    BridgeInfo firstBridgeInfo = null;
+    for (DexProgramClass subclass : subclasses) {
+      DexEncodedMethod definition = subclass.lookupVirtualMethod(method);
+      if (definition == null) {
+        DexEncodedMethod resolutionTarget =
+            appView.appInfo().resolveMethodOnClass(subclass, method).getSingleTarget();
+        if (resolutionTarget == null || resolutionTarget.isAbstract()) {
+          // The fact that this class does not declare the bridge (or the bridge is abstract) should
+          // not prevent us from hoisting the bridge.
+          //
+          // Strictly speaking, there could be an invoke instruction that targets the bridge on this
+          // subclass and fails with an AbstractMethodError or a NoSuchMethodError in the input
+          // program. After hoisting the bridge to the superclass such an instruction would no
+          // longer fail with an error in the generated program.
+          //
+          // If this ever turns out be an issue, it would be possible to track if there is an invoke
+          // instruction targeting the bridge on this subclass that fails in the Enqueuer, but this
+          // should never be the case in practice.
+          continue;
+        }
+        return;
+      }
+
+      BridgeInfo currentBridgeInfo = definition.getOptimizationInfo().getBridgeInfo();
+      if (currentBridgeInfo == null) {
+        return;
+      }
+
+      if (firstBridgeInfo == null) {
+        firstBridgeInfo = currentBridgeInfo;
+      } else if (!currentBridgeInfo.hasSameTarget(firstBridgeInfo)) {
+        return;
+      }
+    }
+
+    // If we reached this point, it is because all of the subclasses define the same bridge.
+    assert firstBridgeInfo != null;
+
+    // Choose one of the bridge definitions as the one that we will be moving to the superclass.
+    ProgramMethod representative = findRepresentative(subclasses, method);
+    assert representative != null;
+
+    // Guard against accessibility issues.
+    if (mayBecomeInaccessibleAfterHoisting(clazz, representative)) {
+      return;
+    }
+
+    // Move the bridge method to the super class, and record this in the graph lens.
+    DexMethod newMethod =
+        appView.dexItemFactory().createMethod(clazz.type, method.proto, method.name);
+    clazz.addVirtualMethod(representative.getMethod().toTypeSubstitutedMethod(newMethod));
+    lensBuilder.move(representative.getMethod().method, newMethod);
+
+    // Remove all of the bridges in the subclasses.
+    for (DexProgramClass subclass : subclasses) {
+      DexEncodedMethod removed = subclass.removeMethod(method);
+      assert removed == null || !appView.appInfo().isPinned(removed.method);
+    }
+  }
+
+  private ProgramMethod findRepresentative(Iterable<DexProgramClass> subclasses, DexMethod method) {
+    for (DexProgramClass subclass : subclasses) {
+      DexEncodedMethod definition = subclass.lookupVirtualMethod(method);
+      if (definition != null) {
+        return new ProgramMethod(subclass, definition);
+      }
+    }
+    return null;
+  }
+
+  private boolean mayBecomeInaccessibleAfterHoisting(
+      DexProgramClass clazz, ProgramMethod representative) {
+    if (clazz.type.isSamePackage(representative.getHolder().type)) {
+      return false;
+    }
+    return !representative.getMethod().isPublic();
+  }
+
+  static class BridgeHoistingLens extends NestedGraphLense {
+
+    public BridgeHoistingLens(
+        AppView<?> appView, BiMap<DexMethod, DexMethod> originalMethodSignatures) {
+      super(
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          ImmutableMap.of(),
+          null,
+          originalMethodSignatures,
+          appView.graphLense(),
+          appView.dexItemFactory());
+    }
+
+    @Override
+    public boolean isLegitimateToHaveEmptyMappings() {
+      return true;
+    }
+
+    static class Builder {
+
+      private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+
+      public boolean isEmpty() {
+        return originalMethodSignatures.isEmpty();
+      }
+
+      public void move(DexMethod from, DexMethod to) {
+        originalMethodSignatures.forcePut(to, originalMethodSignatures.getOrDefault(from, from));
+      }
+
+      public BridgeHoistingLens build(AppView<?> appView) {
+        assert !isEmpty();
+        return new BridgeHoistingLens(appView, originalMethodSignatures);
+      }
+    }
+  }
+}
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 f847e9c..c252c04 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -46,6 +46,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.PredicateSet;
+import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.Visibility;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
@@ -745,6 +746,14 @@
     return result;
   }
 
+  public Set<DexType> getDeadProtoTypes() {
+    return deadProtoTypes;
+  }
+
+  public Set<DexType> getMissingTypes() {
+    return missingTypes;
+  }
+
   public EnumValueInfoMapCollection getEnumValueInfoMapCollection() {
     assert checkIfObsolete();
     return enumValueInfoMaps;
@@ -928,7 +937,6 @@
     return Collections.unmodifiableSortedMap(result);
   }
 
-  @Override
   public boolean isInstantiatedInterface(DexProgramClass clazz) {
     assert checkIfObsolete();
     return objectAllocationInfoCollection.isInterfaceWithUnknownSubtypeHierarchy(clazz);
@@ -1377,4 +1385,47 @@
           || isInstantiatedInterface(clazz.asProgramClass());
     }
   }
+
+  public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(ClassTypeElement type) {
+    // Special case for java.lang.Object.
+    if (type.getClassType() == dexItemFactory().objectType) {
+      if (type.getInterfaces().isEmpty()) {
+        // The type java.lang.Object could be any instantiated type. Assume a finalizer exists.
+        return true;
+      }
+      for (DexType iface : type.getInterfaces()) {
+        if (mayHaveFinalizer(iface)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    return mayHaveFinalizer(type.getClassType());
+  }
+
+  private boolean mayHaveFinalizer(DexType type) {
+    // A type may have an active finalizer if any derived instance has a finalizer.
+    return objectAllocationInfoCollection
+        .traverseInstantiatedSubtypes(
+            type,
+            clazz -> {
+              if (objectAllocationInfoCollection.isInterfaceWithUnknownSubtypeHierarchy(clazz)) {
+                return TraversalContinuation.BREAK;
+              } else {
+                SingleResolutionResult resolution =
+                    resolveMethod(clazz, dexItemFactory().objectMembers.finalize)
+                        .asSingleResolution();
+                if (resolution != null && resolution.getResolvedHolder().isProgramClass()) {
+                  return TraversalContinuation.BREAK;
+                }
+              }
+              return TraversalContinuation.CONTINUE;
+            },
+            lambda -> {
+              // Lambda classes do not have finalizers.
+              return TraversalContinuation.CONTINUE;
+            },
+            this)
+        .shouldBreak();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
index 0f1b348..32a8501 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLivenessModifier.java
@@ -33,14 +33,9 @@
 
   public void modify(AppInfoWithLiveness appInfo) {
     // Instantiated classes.
+    noLongerInstantiatedClasses.forEach(appInfo::removeFromSingleTargetLookupCache);
     appInfo.mutateObjectAllocationInfoCollection(
-        mutator -> {
-          noLongerInstantiatedClasses.forEach(
-              clazz -> {
-                mutator.markNoLongerInstantiated(clazz);
-                appInfo.removeFromSingleTargetLookupCache(clazz);
-              });
-        });
+        mutator -> noLongerInstantiatedClasses.forEach(mutator::markNoLongerInstantiated));
     // Written fields.
     FieldAccessInfoCollectionImpl fieldAccessInfoCollection =
         appInfo.getMutableFieldAccessInfoCollection();
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 4b599bf..1319195 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -228,6 +228,12 @@
   /** Set of missing types. */
   private final Set<DexType> missingTypes = Sets.newIdentityHashSet();
 
+  /** Set of proto types that were found to be dead during the first round of tree shaking. */
+  private Set<DexType> initialDeadProtoTypes;
+
+  /** Set of types that were found to be missing during the first round of tree shaking. */
+  private Set<DexType> initialMissingTypes;
+
   /** Mapping from each unused interface to the set of live types that implements the interface. */
   private final Map<DexProgramClass, Set<DexProgramClass>> unusedInterfaceTypes =
       new IdentityHashMap<>();
@@ -416,6 +422,16 @@
     this.annotationRemoverBuilder = annotationRemoverBuilder;
   }
 
+  public void setInitialDeadProtoTypes(Set<DexType> initialDeadProtoTypes) {
+    assert mode.isFinalTreeShaking();
+    this.initialDeadProtoTypes = initialDeadProtoTypes;
+  }
+
+  public void setInitialMissingTypes(Set<DexType> initialMissingTypes) {
+    assert mode.isFinalTreeShaking();
+    this.initialMissingTypes = initialMissingTypes;
+  }
+
   public void addDeadProtoTypeCandidate(DexType type) {
     assert type.isProgramType(appView);
     addDeadProtoTypeCandidate(appView.definitionFor(type).asProgramClass());
@@ -726,6 +742,12 @@
     return isRead ? info.recordRead(field, context) : info.recordWrite(field, context);
   }
 
+  private boolean isStringConcat(DexMethodHandle bootstrapMethod) {
+    return bootstrapMethod.type.isInvokeStatic()
+        && (bootstrapMethod.asMethod() == appView.dexItemFactory().stringConcatWithConstantsMethod
+            || bootstrapMethod.asMethod() == appView.dexItemFactory().stringConcatMethod);
+  }
+
   void traceCallSite(DexCallSite callSite, ProgramMethod context) {
     DexProgramClass bootstrapClass =
         getProgramClassOrNull(callSite.bootstrapMethod.asMethod().holder);
@@ -736,7 +758,7 @@
     DexProgramClass contextHolder = context.getHolder();
     LambdaDescriptor descriptor = LambdaDescriptor.tryInfer(callSite, appInfo, contextHolder);
     if (descriptor == null) {
-      if (!appInfo.isStringConcat(callSite.bootstrapMethod)) {
+      if (!isStringConcat(callSite.bootstrapMethod)) {
         if (options.reporter != null) {
           Diagnostic message =
               new StringDiagnostic(
@@ -955,10 +977,7 @@
       return appView.withGeneratedMessageLiteBuilderShrinker(
           shrinker ->
               shrinker.deferDeadProtoBuilders(
-                  clazz,
-                  currentMethod,
-                  () -> liveTypes.registerDeferredAction(clazz, action),
-                  this),
+                  clazz, currentMethod, () -> liveTypes.registerDeferredAction(clazz, action)),
           false);
     }
     return false;
@@ -1803,6 +1822,11 @@
   }
 
   private void reportMissingClass(DexType clazz) {
+    assert !mode.isFinalTreeShaking()
+            || appView.dexItemFactory().isPossiblyCompilerSynthesizedType(clazz)
+            || (initialDeadProtoTypes != null && initialDeadProtoTypes.contains(clazz))
+            || initialMissingTypes.contains(clazz)
+        : "Unexpected missing class `" + clazz.toSourceString() + "`";
     boolean newReport = missingTypes.add(clazz);
     if (Log.ENABLED && newReport) {
       Log.verbose(Enqueuer.class, "Class `%s` is missing.", clazz);
@@ -3512,7 +3536,10 @@
       return;
     }
     if (identifierItem.isDexType()) {
-      DexProgramClass clazz = getProgramClassOrNull(identifierItem.asDexType());
+      // This is using appView.definitionFor() to avoid that we report reflectively accessed types
+      // as missing.
+      DexProgramClass clazz =
+          asProgramClassOrNull(appView.definitionFor(identifierItem.asDexType()));
       if (clazz == null) {
         return;
       }
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
index e716083..a949527 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
@@ -7,7 +7,9 @@
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.Enqueuer.Mode;
+import java.util.Set;
 
 public class EnqueuerFactory {
 
@@ -17,8 +19,14 @@
   }
 
   public static Enqueuer createForFinalTreeShaking(
-      AppView<? extends AppInfoWithSubtyping> appView, GraphConsumer keptGraphConsumer) {
-    return new Enqueuer(appView, keptGraphConsumer, Mode.FINAL_TREE_SHAKING);
+      AppView<? extends AppInfoWithSubtyping> appView,
+      GraphConsumer keptGraphConsumer,
+      Set<DexType> initialMissingTypes) {
+    Enqueuer enqueuer = new Enqueuer(appView, keptGraphConsumer, Mode.FINAL_TREE_SHAKING);
+    appView.withProtoShrinker(
+        shrinker -> enqueuer.setInitialDeadProtoTypes(shrinker.getDeadProtoTypes()));
+    enqueuer.setInitialMissingTypes(initialMissingTypes);
+    return enqueuer;
   }
 
   public static Enqueuer createForMainDexTracing(AppView<? extends AppInfoWithSubtyping> appView) {
diff --git a/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java b/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
index 1691758..a2f246e 100644
--- a/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
+++ b/src/main/java/com/android/tools/r8/shaking/SingleTargetLookupCache.java
@@ -4,11 +4,13 @@
 
 package com.android.tools.r8.shaking;
 
-import com.android.tools.r8.graph.DexClass;
 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.utils.TraversalContinuation;
+import com.google.common.collect.Sets;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class SingleTargetLookupCache {
@@ -25,14 +27,25 @@
   }
 
   public void removeInstantiatedType(DexType instantiatedType, AppInfoWithLiveness appInfo) {
-    // Remove all types in the hierarchy related to this type.
+    // Remove all types in the instantiated hierarchy related to this type.
     cache.remove(instantiatedType);
-    DexClass clazz = appInfo.definitionFor(instantiatedType);
-    if (clazz == null) {
-      return;
-    }
-    appInfo.forEachSuperType(clazz, (type, ignore) -> cache.remove(type));
-    appInfo.subtypes(instantiatedType).forEach(cache::remove);
+    Set<DexType> seen = Sets.newIdentityHashSet();
+    appInfo.forEachInstantiatedSubType(
+        instantiatedType,
+        instance ->
+            appInfo.traverseSuperTypes(
+                instance,
+                (type, ignore) -> {
+                  if (seen.add(type)) {
+                    cache.remove(type);
+                    return TraversalContinuation.CONTINUE;
+                  } else {
+                    return TraversalContinuation.BREAK;
+                  }
+                }),
+        lambda -> {
+          assert false;
+        });
   }
 
   public DexEncodedMethod getCachedItem(DexType receiverType, DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 40324ec..4ce026a 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -7,6 +7,7 @@
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.function.Function;
+import java.util.function.IntPredicate;
 import java.util.function.Predicate;
 
 public class ArrayUtils {
@@ -106,6 +107,14 @@
         clazz.cast(Array.newInstance(clazz.getComponentType(), results.size())));
   }
 
+  public static int[] createIdentityArray(int size) {
+    int[] array = new int[size];
+    for (int i = 0; i < size; i++) {
+      array[i] = i;
+    }
+    return array;
+  }
+
   public static <T> boolean contains(T[] elements, T elementToLookFor) {
     for (Object element : elements) {
       if (element.equals(elementToLookFor)) {
@@ -114,4 +123,18 @@
     }
     return false;
   }
+
+  public static int[] fromPredicate(IntPredicate predicate, int size) {
+    int[] result = new int[size];
+    for (int i = 0; i < size; i++) {
+      result[i] = BooleanUtils.intValue(predicate.test(i));
+    }
+    return result;
+  }
+
+  public static void sumOfPredecessorsInclusive(int[] array) {
+    for (int i = 1; i < array.length; i++) {
+      array[i] += array[i - 1];
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index de25a3b..196091e 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -355,7 +355,7 @@
    */
   public static String getDescriptorFromClassBinaryName(String typeBinaryName) {
     assert typeBinaryName != null;
-    return ('L' + typeBinaryName + ';');
+    return 'L' + typeBinaryName + ';';
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/utils/ForEachable.java b/src/main/java/com/android/tools/r8/utils/ForEachable.java
new file mode 100644
index 0000000..4c8ae53
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ForEachable.java
@@ -0,0 +1,11 @@
+// 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 java.util.function.Consumer;
+
+public interface ForEachable<T> {
+
+  void forEach(Consumer<T> consumer);
+}
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 6ab3fcd..b58e9d6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.errors.InvalidDebugInfoException;
 import com.android.tools.r8.errors.InvalidLibrarySuperclassDiagnostic;
 import com.android.tools.r8.errors.MissingNestHostNestDesugarDiagnostic;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppView;
@@ -162,7 +163,6 @@
   void enableProtoShrinking() {
     applyInliningToInlinee = true;
     enableFieldBitAccessAnalysis = true;
-    enableStringSwitchConversion = true;
     protoShrinking.enableGeneratedMessageLiteShrinking = true;
     protoShrinking.enableGeneratedMessageLiteBuilderShrinking = true;
     protoShrinking.enableGeneratedExtensionRegistryShrinking = true;
@@ -273,8 +273,8 @@
   // the actual catch handler allowed when inlining. Threshold found empirically by testing on
   // GMS Core.
   public int inliningControlFlowResolutionBlocksThreshold = 15;
-  public boolean enableStringSwitchConversion =
-      System.getProperty("com.android.tools.r8.stringSwitchConversion") != null;
+  public boolean enableStringSwitchConversion = true;
+  public int minimumStringSwitchSize = 3;
   public boolean enableEnumValueOptimization = true;
   public boolean enableEnumSwitchMapRemoval = true;
   public final OutlineOptions outline = new OutlineOptions();
@@ -701,6 +701,12 @@
     return assertionsEnabled;
   }
 
+  public static void checkAssertionsEnabled() {
+    if (!assertionsEnabled()) {
+      throw new Unreachable();
+    }
+  }
+
   /** A set of dexitems we have reported missing to dedupe warnings. */
   private final Set<DexItem> reportedMissingForDesugaring = Sets.newConcurrentHashSet();
 
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index fdf7e98..02e83de 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -11,6 +11,8 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class StringUtils {
   public static char[] EMPTY_CHAR_ARRAY = {};
@@ -327,4 +329,8 @@
     }
     return string.length();
   }
+
+  public static String replaceAll(String subject, String target, String replacement) {
+    return subject.replaceAll(Pattern.quote(target), Matcher.quoteReplacement(replacement));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
index 84aa0d0..e6076ca 100644
--- a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
@@ -21,32 +21,37 @@
   public static <T, R, E extends Exception> Collection<R> processItemsWithResults(
       Iterable<T> items, ThrowingFunction<T, R, E> consumer, ExecutorService executorService)
       throws ExecutionException {
+    return processItemsWithResults(items::forEach, consumer, executorService);
+  }
+
+  public static <T, R, E extends Exception> Collection<R> processItemsWithResults(
+      ForEachable<T> items, ThrowingFunction<T, R, E> consumer, ExecutorService executorService)
+      throws ExecutionException {
     List<Future<R>> futures = new ArrayList<>();
-    for (T item : items) {
-      futures.add(executorService.submit(() -> consumer.apply(item)));
-    }
+    items.forEach(item -> futures.add(executorService.submit(() -> consumer.apply(item))));
     return awaitFuturesWithResults(futures);
   }
 
   public static <T, E extends Exception> void processItems(
       Iterable<T> items, ThrowingConsumer<T, E> consumer, ExecutorService executorService)
       throws ExecutionException {
-    processItemsWithResults(
-        items,
-        arg -> {
-          consumer.accept(arg);
-          return null;
-        },
-        executorService);
+    processItems(items::forEach, consumer, executorService);
   }
 
   public static <T, U, E extends Exception> void processItems(
       Map<T, U> items, ThrowingBiConsumer<T, U, E> consumer, ExecutorService executorService)
       throws ExecutionException {
+    processItems(
+        items.entrySet(), arg -> consumer.accept(arg.getKey(), arg.getValue()), executorService);
+  }
+
+  public static <T, E extends Exception> void processItems(
+      ForEachable<T> items, ThrowingConsumer<T, E> consumer, ExecutorService executorService)
+      throws ExecutionException {
     processItemsWithResults(
-        items.entrySet(),
+        items,
         arg -> {
-          consumer.accept(arg.getKey(), arg.getValue());
+          consumer.accept(arg);
           return null;
         },
         executorService);
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index ac15d64..743a898 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.graph.SmaliWriter;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
@@ -235,8 +236,8 @@
     return ClassFileTransformer.create(clazz);
   }
 
-  public static ClassFileTransformer transformer(byte[] clazz) {
-    return ClassFileTransformer.create(clazz);
+  public static ClassFileTransformer transformer(byte[] bytes, ClassReference classReference) {
+    return ClassFileTransformer.create(bytes, classReference);
   }
 
   // Actually running Proguard should only be during development.
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index e184d97..080051f 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1061,6 +1061,11 @@
     return paths;
   }
 
+  public static Collection<Path> getClassFilesForInnerClasses(Class<?>... classes)
+      throws IOException {
+    return getClassFilesForInnerClasses(Arrays.asList(classes));
+  }
+
   public static Path getFileNameForTestClass(Class clazz) {
     List<String> parts = getNamePartsForTestClass(clazz);
     return Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY));
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
index 9fe3bb4..2c46b40 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.bridgeremoval;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
@@ -141,18 +142,24 @@
     // Cls1#foo and Cls2#foo should not refer to each other.
     // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
 
-    MethodSubject fooInCls2 =
-        cls2Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
-    assertThat(fooInCls2, isPresent());
-    DexCode code = fooInCls2.getMethod().getCode().asDexCode();
+    // Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+    MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
+    assertThat(fooInCls2, not(isPresent()));
+
+    MethodSubject fooFromCls2InAbsCls = absSubject.method("void", "foo", "java.lang.Integer");
+    assertThat(fooFromCls2InAbsCls, isPresent());
+    DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
     InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
     assertEquals(absSubject.getDexClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooInCls1 =
-        cls1Subject.method("void", "foo", ImmutableList.of("java.lang.String"));
-    assertThat(fooInCls1, isPresent());
-    code = fooInCls1.getMethod().getCode().asDexCode();
+    // Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
+    assertThat(fooInCls1, not(isPresent()));
+
+    MethodSubject fooFromCls1InAbsCls = absSubject.method("void", "foo", "java.lang.String");
+    assertThat(fooFromCls1InAbsCls, isPresent());
+    code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
     invoke = (InvokeVirtual) code.instructions[0];
     assertEquals(absSubject.getDexClass().type, invoke.getMethod().holder);
@@ -247,18 +254,20 @@
 
     // Cls1#foo and Cls2#bar should refer to Base#foo.
 
-    MethodSubject barInCls2 =
-        cls2Subject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
     assertThat(barInCls2, isPresent());
     DexCode code = barInCls2.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
     InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
     assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooInCls1 =
-        cls1Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
-    assertThat(fooInCls1, isPresent());
-    code = fooInCls1.getMethod().getCode().asDexCode();
+    // Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
+    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
+    assertThat(fooInCls1, not(isPresent()));
+
+    MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
+    assertThat(fooInBase, isPresent());
+    code = fooInBase.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
     invoke = (InvokeVirtual) code.instructions[0];
     assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
@@ -341,8 +350,7 @@
 
     // DerivedString2#bar should refer to Base#foo.
 
-    MethodSubject barInSub =
-        subSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
     assertThat(barInSub, isPresent());
     DexCode code = barInSub.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
@@ -415,8 +423,7 @@
 
     // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
 
-    MethodSubject barInSub =
-        baseSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
     assertThat(barInSub, isPresent());
     DexCode code = barInSub.getMethod().getCode().asDexCode();
     checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
index ebb7662..16db7d8 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
@@ -61,6 +61,7 @@
     ClassSubject aClassSubject = inspector.clazz(BridgeHoistingAccessibilityTestClasses.A.class);
     assertThat(aClassSubject, isPresent());
     assertThat(aClassSubject.uniqueMethodWithName("m"), isPresent());
+    assertThat(aClassSubject.uniqueMethodWithName("bridgeC"), isPresent());
 
     ClassSubject bClassSubject = inspector.clazz(B.class);
     assertThat(bClassSubject, isPresent());
@@ -68,8 +69,6 @@
 
     ClassSubject cClassSubject = inspector.clazz(C.class);
     assertThat(cClassSubject, isPresent());
-    // TODO(b/153147967): Should be hoisted to A.
-    assertThat(cClassSubject.uniqueMethodWithName("bridgeC"), isPresent());
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonReboundBridgeHoistingTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonReboundBridgeHoistingTest.java
index 362f6a2..d6eb6cd 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonReboundBridgeHoistingTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/NonReboundBridgeHoistingTest.java
@@ -60,11 +60,11 @@
 
     ClassSubject bClassSubject = inspector.clazz(NonReboundBridgeHoistingTestClasses.B.class);
     assertThat(bClassSubject, isPresent());
+    assertThat(bClassSubject.uniqueMethodWithName("bridge"), isPresent());
 
     ClassSubject cClassSubject = inspector.clazz(C.class);
     assertThat(cClassSubject, isPresent());
-    // TODO(b/153147967): This bridge should be hoisted to B.
-    assertThat(cClassSubject.uniqueMethodWithName("bridge"), isPresent());
+    assertThat(cClassSubject.uniqueMethodWithName("bridge"), not(isPresent()));
   }
 
   static class TestClass {
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 50fc23c..89437f1 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
@@ -59,24 +59,18 @@
     ClassSubject aClassSubject = inspector.clazz(A.class);
     assertThat(aClassSubject, isPresent());
     assertThat(aClassSubject.uniqueMethodWithName("m"), isPresent());
-    // TODO(b/153147967): This should become present after hoisting.
-    assertThat(aClassSubject.uniqueMethodWithName("superBridge"), not(isPresent()));
-    // TODO(b/153147967): This should become present after hoisting.
-    assertThat(aClassSubject.uniqueMethodWithName("virtualBridge"), not(isPresent()));
+    assertThat(aClassSubject.uniqueMethodWithName("superBridge"), isPresent());
+    assertThat(aClassSubject.uniqueMethodWithName("virtualBridge"), isPresent());
 
     ClassSubject b1ClassSubject = inspector.clazz(B1.class);
     assertThat(b1ClassSubject, isPresent());
-    // TODO(b/153147967): This bridge should be hoisted to A.
-    assertThat(b1ClassSubject.uniqueMethodWithName("superBridge"), isPresent());
-    // TODO(b/153147967): This bridge should be hoisted to A.
-    assertThat(b1ClassSubject.uniqueMethodWithName("virtualBridge"), isPresent());
+    assertThat(b1ClassSubject.uniqueMethodWithName("superBridge"), not(isPresent()));
+    assertThat(b1ClassSubject.uniqueMethodWithName("virtualBridge"), not(isPresent()));
 
     ClassSubject b2ClassSubject = inspector.clazz(B2.class);
     assertThat(b2ClassSubject, isPresent());
-    // TODO(b/153147967): This bridge should be hoisted to A.
-    assertThat(b2ClassSubject.uniqueMethodWithName("superBridge"), isPresent());
-    // TODO(b/153147967): This bridge should be hoisted to A.
-    assertThat(b2ClassSubject.uniqueMethodWithName("virtualBridge"), isPresent());
+    assertThat(b2ClassSubject.uniqueMethodWithName("superBridge"), not(isPresent()));
+    assertThat(b2ClassSubject.uniqueMethodWithName("virtualBridge"), not(isPresent()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java b/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
index 8096c47..e6fdf99 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
@@ -35,6 +35,9 @@
         System.out.println("Skipping check for " + apiLevel);
         continue;
       }
+      if (apiLevel == AndroidApiLevel.R) {
+        continue;
+      }
       // Check that the backported methods for each API level are are not present in the
       // android.jar for that level.
       CodeInspector inspector = new CodeInspector(ToolHelper.getAndroidJar(apiLevel));
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java
new file mode 100644
index 0000000..eefe0f7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java
@@ -0,0 +1,145 @@
+// 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.desugaredlibrary;
+
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.desugar.desugaredlibrary.jdktests.Jdk11DesugaredLibraryTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MergingWithDesugaredLibraryTest extends Jdk11DesugaredLibraryTestBase {
+
+  private static final String JAVA_RESULT = "java.util.stream.ReferencePipeline$Head";
+  private static final String J$_RESULT = "j$.util.stream.ReferencePipeline$Head";
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public MergingWithDesugaredLibraryTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testMergeDesugaredAndNonDesugared() throws Exception {
+    D8TestCompileResult compileResult =
+        testForD8()
+            .addProgramFiles(buildPart1DesugaredLibrary(), buildPart2NoDesugaredLibrary())
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .setMinApi(parameters.getApiLevel())
+            .enableCoreLibraryDesugaring(parameters.getApiLevel())
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary, parameters.getApiLevel());
+    // TODO(b/154106502): This should raise a proper warning. The dex files are incompatible,
+    //  so the behavior is undefined regarding desugared types.
+    if (parameters.getApiLevel().getLevel() < AndroidApiLevel.N.getLevel()) {
+      compileResult
+          .run(parameters.getRuntime(), Part1.class)
+          .assertSuccessWithOutputLines(J$_RESULT);
+    } else {
+      compileResult
+          .run(parameters.getRuntime(), Part1.class)
+          .assertSuccessWithOutputLines(JAVA_RESULT);
+    }
+    if (parameters.getRuntime().asDex().getMinApiLevel().getLevel()
+        < AndroidApiLevel.N.getLevel()) {
+      compileResult
+          .run(parameters.getRuntime(), Part2.class)
+          .assertFailureWithErrorThatMatches(containsString("java.lang.NoSuchMethodError"));
+    } else {
+
+      compileResult
+          .run(parameters.getRuntime(), Part2.class)
+          .assertSuccessWithOutputLines(JAVA_RESULT);
+    }
+  }
+
+  @Test
+  public void testMergeDesugaredAndClassFile() throws Exception {
+    D8TestCompileResult compileResult =
+        testForD8()
+            .addProgramFiles(buildPart1DesugaredLibrary())
+            .addProgramClasses(Part2.class)
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .setMinApi(parameters.getApiLevel())
+            .enableCoreLibraryDesugaring(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(this::assertWarningPresent)
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary, parameters.getApiLevel());
+    if (parameters.getApiLevel().getLevel() < AndroidApiLevel.N.getLevel()) {
+      compileResult
+          .run(parameters.getRuntime(), Part1.class)
+          .assertSuccessWithOutputLines(J$_RESULT);
+      compileResult
+          .run(parameters.getRuntime(), Part2.class)
+          .assertSuccessWithOutputLines(J$_RESULT);
+    } else {
+      compileResult
+          .run(parameters.getRuntime(), Part1.class)
+          .assertSuccessWithOutputLines(JAVA_RESULT);
+      compileResult
+          .run(parameters.getRuntime(), Part2.class)
+          .assertSuccessWithOutputLines(JAVA_RESULT);
+    }
+  }
+
+  private void assertWarningPresent(TestDiagnosticMessages testDiagnosticMessages) {
+    if (parameters.getApiLevel().getLevel() > AndroidApiLevel.N.getLevel()) {
+      return;
+    }
+    assertTrue(
+        testDiagnosticMessages.getWarnings().stream()
+            .anyMatch(
+                warn -> warn.getDiagnosticMessage().startsWith("The compilation is slowed down")));
+  }
+
+  private Path buildPart1DesugaredLibrary() throws Exception {
+    return testForD8()
+        .addProgramClasses(Part1.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .compile()
+        .writeToZip();
+  }
+
+  private Path buildPart2NoDesugaredLibrary() throws Exception {
+    return testForD8()
+        .addProgramClasses(Part2.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .writeToZip();
+  }
+
+  @SuppressWarnings("RedundantOperationOnEmptyContainer")
+  static class Part1 {
+    public static void main(String[] args) {
+      System.out.println(new ArrayList<>().stream().getClass().getName());
+    }
+  }
+
+  @SuppressWarnings("RedundantOperationOnEmptyContainer")
+  static class Part2 {
+    public static void main(String[] args) {
+      System.out.println(new ArrayList<>().stream().getClass().getName());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionAndMergeTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionAndMergeTest.java
new file mode 100644
index 0000000..ef584d2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/ConversionAndMergeTest.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.desugar.desugaredlibrary.conversiontests;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.time.ZoneId;
+import java.util.TimeZone;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConversionAndMergeTest extends DesugaredLibraryTestBase {
+
+  private static final String GMT = StringUtils.lines("GMT");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getConversionParametersUpToExcluding(AndroidApiLevel.O);
+  }
+
+  public ConversionAndMergeTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testMerge() throws Exception {
+    Path extra = buildClass(ExtraClass.class);
+    Path convClass = buildClass(APIConversionClass.class);
+    testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addProgramFiles(extra, convClass)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(this::buildDesugaredLibrary, parameters.getApiLevel())
+        .run(parameters.getRuntime(), APIConversionClass.class)
+        .assertSuccessWithOutput(GMT);
+  }
+
+  private Path buildClass(Class<?> cls) throws Exception {
+    return testForD8()
+        .setMinApi(parameters.getApiLevel())
+        .addProgramClasses(cls)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .compile()
+        .writeToZip();
+  }
+
+  static class ExtraClass {
+    public static void main(String[] args) {
+      System.out.println("Hello world!");
+    }
+  }
+
+  static class APIConversionClass {
+    public static void main(String[] args) {
+      // Following is a call where java.time.ZoneId is a parameter type (getTimeZone()).
+      TimeZone timeZone = TimeZone.getTimeZone(ZoneId.systemDefault());
+      // Following is a call where java.time.ZoneId is a return type (toZoneId()).
+      System.out.println(timeZone.toZoneId().getId());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/NullOutValueInvokeEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/NullOutValueInvokeEnumUnboxingTest.java
new file mode 100644
index 0000000..c65427a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/NullOutValueInvokeEnumUnboxingTest.java
@@ -0,0 +1,80 @@
+// 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.enumunboxing;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import 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 NullOutValueInvokeEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private static final Class<?> ENUM_CLASS = MyEnum.class;
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final KeepRule enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public NullOutValueInvokeEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, KeepRule enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> classToTest = NullOutValueInvoke.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classToTest, ENUM_CLASS)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(enumKeepRules.getKeepRule())
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class NullOutValueInvoke {
+
+    public static void main(String[] args) {
+      printAndGetMyEnum();
+      printAndGetMyEnum();
+    }
+
+    @SuppressWarnings("UnusedReturnValue")
+    @NeverInline
+    static MyEnum printAndGetMyEnum() {
+      System.out.println("print");
+      return MyEnum.B;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/PinnedEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/PinnedEnumUnboxingTest.java
index 355c6a1..fff500b 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/PinnedEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/PinnedEnumUnboxingTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.enumunboxing;
 
 import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import java.util.List;
 import org.junit.Test;
@@ -15,7 +16,7 @@
 @RunWith(Parameterized.class)
 public class PinnedEnumUnboxingTest extends EnumUnboxingTestBase {
 
-  private static final Class<?> ENUM_CLASS = MyEnum.class;
+  private static final Class<?>[] BOXED = {MainWithKeptEnum.class, MainWithKeptEnumArray.class};
 
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
@@ -35,34 +36,56 @@
 
   @Test
   public void testEnumUnboxing() throws Exception {
-    Class<MainWithKeptEnum> classToTest = MainWithKeptEnum.class;
-    testForR8(parameters.getBackend())
-        .addProgramClasses(classToTest, ENUM_CLASS)
-        .addKeepMainRule(classToTest)
-        .addKeepClassRules(MyEnum.class)
-        .addKeepRules(enumKeepRules.getKeepRule())
-        .enableNeverClassInliningAnnotations()
-        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
-        .allowDiagnosticInfoMessages()
-        .setMinApi(parameters.getApiLevel())
-        .compile()
-        .inspectDiagnosticMessages(
-            m -> assertEnumIsBoxed(ENUM_CLASS, classToTest.getSimpleName(), m))
-        .run(parameters.getRuntime(), classToTest)
-        .assertSuccessWithOutputLines("0");
-  }
-
-  @NeverClassInline
-  enum MyEnum {
-    A,
-    B,
-    C
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(PinnedEnumUnboxingTest.class)
+            .addKeepMainRules(BOXED)
+            .addKeepClassRules(MainWithKeptEnum.MyEnum.class)
+            .addKeepMethodRules(MainWithKeptEnumArray.class, "keptMethod()")
+            .addKeepRules(enumKeepRules.getKeepRule())
+            .enableNeverClassInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile();
+    for (Class<?> boxed : BOXED) {
+      compileResult
+          .inspectDiagnosticMessages(
+              m -> assertEnumIsBoxed(boxed.getDeclaredClasses()[0], boxed.getSimpleName(), m))
+          .run(parameters.getRuntime(), boxed)
+          .assertSuccessWithOutputLines("0");
+    }
   }
 
   static class MainWithKeptEnum {
 
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
     public static void main(String[] args) {
       System.out.println(MyEnum.A.ordinal());
     }
   }
+
+  static class MainWithKeptEnumArray {
+    @NeverClassInline
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+    }
+
+    public static MyEnum[] keptMethod() {
+      System.out.println("KEPT");
+      return null;
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java b/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java
index 9644e0d..2a700c3 100644
--- a/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/StringSwitchConversionFromIfTest.java
@@ -24,7 +24,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Streams;
 import java.util.List;
-import java.util.Objects;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,7 +37,8 @@
 
   @Parameterized.Parameters(name = "{1}, enable string switch conversion: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public StringSwitchConversionFromIfTest(
@@ -53,15 +53,12 @@
         .addInnerClasses(StringSwitchConversionFromIfTest.class)
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
-            options -> {
-              assert !options.enableStringSwitchConversion;
-              options.enableStringSwitchConversion = enableStringSwitchConversion;
-            })
+            options -> options.enableStringSwitchConversion = enableStringSwitchConversion)
         .enableInliningAnnotations()
         // TODO(b/135560746): Add support for treating the keys of a string-switch instruction as an
         //  identifier name string.
         .noMinification()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
             inspector -> {
@@ -96,15 +93,6 @@
                 // compiled to a sequence of `if (x.equals("..."))` instructions that do not even
                 // use the hash code.
                 assertNotEquals(0, hashCodeValues.size());
-
-                // We indirectly verify that the string-switch instructions have been identified
-                // by checking that none of the hash codes are used for anything. Note that this
-                // only holds with a backend that compiles string-switch instruction to
-                // `if (x.equals("..."))` instructions that do not use the hash code for anything.
-                assertEquals(
-                    methodName,
-                    enableStringSwitchConversion,
-                    hashCodeValues.stream().filter(Objects::nonNull).noneMatch(Value::isUsed));
               }
             })
         .run(parameters.getRuntime(), TestClass.class)
@@ -139,25 +127,28 @@
     private static final int HASH_2 = -1340982898;
 
     public static void main(String[] args) {
-      test(A.class);
-      test(B.class);
-      testWithDeadStringIdComparisons(A.class);
-      testWithDeadStringIdComparisons(B.class);
-      testWithMultipleHashCodeInvocations(A.class);
-      testWithMultipleHashCodeInvocations(B.class);
-      testWithSwitch(A.class);
-      testWithSwitch(B.class);
+      test(toNullableClassName(A.class));
+      test(toNullableClassName(B.class));
+      testWithDeadStringIdComparisons(toNullableClassName(A.class));
+      testWithDeadStringIdComparisons(toNullableClassName(B.class));
+      testWithMultipleHashCodeInvocations(toNullableClassName(A.class));
+      testWithMultipleHashCodeInvocations(toNullableClassName(B.class));
+      testWithSwitch(toNullableClassName(A.class));
+      testWithSwitch(toNullableClassName(B.class));
 
       try {
-        testNullCheckIsPreserved(System.currentTimeMillis() >= 0 ? null : A.class);
+        testNullCheckIsPreserved(System.currentTimeMillis() >= 0 ? null : A.class.getName());
       } catch (NullPointerException e) {
         System.out.println("Caught NPE");
       }
     }
 
+    static String toNullableClassName(Class<?> clazz) {
+      return System.currentTimeMillis() > 0 ? clazz.getName() : null;
+    }
+
     @NeverInline
-    private static void test(Class<?> clazz) {
-      String className = clazz.getName();
+    private static void test(String className) {
       int hashCode = className.hashCode();
       int result = -1;
       if (hashCode == HASH_1) {
@@ -177,8 +168,7 @@
     }
 
     @NeverInline
-    private static void testWithDeadStringIdComparisons(Class<?> clazz) {
-      String className = clazz.getName();
+    private static void testWithDeadStringIdComparisons(String className) {
       int hashCode = className.hashCode();
       int result = -1;
       if (hashCode == HASH_1) {
@@ -222,8 +212,7 @@
     }
 
     @NeverInline
-    private static void testWithMultipleHashCodeInvocations(Class<?> clazz) {
-      String className = clazz.getName();
+    private static void testWithMultipleHashCodeInvocations(String className) {
       int result = -1;
       if (className.hashCode() == HASH_1) {
         if (className.equals(NAME_1)) {
@@ -242,8 +231,7 @@
     }
 
     @NeverInline
-    private static void testWithSwitch(Class<?> clazz) {
-      String className = clazz.getName();
+    private static void testWithSwitch(String className) {
       int result = -1;
       if (className.hashCode() == HASH_1) {
         if (className.equals(NAME_1)) {
@@ -267,8 +255,8 @@
     }
 
     @NeverInline
-    private static void testNullCheckIsPreserved(Class<?> clazz) {
-      switch (clazz.getName()) {
+    private static void testNullCheckIsPreserved(String className) {
+      switch (className) {
         case "A":
           System.out.println("Unexpected (A)");
           break;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/ConvertRemovedStringSwitchTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/ConvertRemovedStringSwitchTest.java
new file mode 100644
index 0000000..3243171
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/ConvertRemovedStringSwitchTest.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.switches;
+
+import static com.android.tools.r8.utils.BooleanUtils.intValue;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+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.InstructionSubject.JumboStringMode;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConvertRemovedStringSwitchTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ConvertRemovedStringSwitchTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(ConvertRemovedStringSwitchTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class, "A", "B", "C", "D", "E")
+        .assertSuccessWithOutputLines("A", "B", "C", "D", "E!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(TestClass.class);
+    assertThat(classSubject, isPresent());
+
+    MethodSubject mainMethodSubject = classSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+
+    // Verify that the keys were canonicalized.
+    Object2IntMap<String> stringCounts = countStrings(mainMethodSubject);
+    assertEquals(1 + intValue(parameters.isCfRuntime()), stringCounts.getInt("A"));
+    assertEquals(1 + intValue(parameters.isCfRuntime()), stringCounts.getInt("B"));
+    assertEquals(1 + intValue(parameters.isCfRuntime()), stringCounts.getInt("C"));
+    assertEquals(1 + intValue(parameters.isCfRuntime()), stringCounts.getInt("D"));
+    assertEquals(1, stringCounts.getInt("E"));
+    assertEquals(1, stringCounts.getInt("E!"));
+
+    // Verify that we can rebuild the StringSwitch instruction.
+    IRCode code = mainMethodSubject.buildIR();
+    assertTrue(code.streamInstructions().anyMatch(Instruction::isStringSwitch));
+  }
+
+  private static Object2IntMap<String> countStrings(MethodSubject methodSubject) {
+    Object2IntMap<String> result = new Object2IntOpenHashMap<>();
+    methodSubject
+        .streamInstructions()
+        .filter(instruction -> instruction.isConstString(JumboStringMode.ALLOW))
+        .map(InstructionSubject::getConstString)
+        .forEach(string -> result.put(string, result.getInt(string) + 1));
+    return result;
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      for (String arg : args) {
+        switch (arg) {
+          case "A":
+            System.out.println("A");
+            break;
+          case "B":
+            System.out.println("B");
+            break;
+          case "C":
+            System.out.println("C");
+            break;
+          case "D":
+            System.out.println("D");
+            break;
+          case "E":
+            // Intentionally "E!" to prevent canonicalization of this key.
+            System.out.println("E!");
+            break;
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchCaseRemovalWithCompileTimeHashCodeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchCaseRemovalWithCompileTimeHashCodeTest.java
index ff41e06..7660e79 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchCaseRemovalWithCompileTimeHashCodeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchCaseRemovalWithCompileTimeHashCodeTest.java
@@ -6,13 +6,15 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject.JumboStringMode;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -20,14 +22,19 @@
 
 @RunWith(Parameterized.class)
 public class StringSwitchCaseRemovalWithCompileTimeHashCodeTest extends TestBase {
+
+  private final boolean enableStringSwitchConversion;
   private final TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+  @Parameters(name = "{1}, enable string-switch conversion: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public StringSwitchCaseRemovalWithCompileTimeHashCodeTest(TestParameters parameters) {
+  public StringSwitchCaseRemovalWithCompileTimeHashCodeTest(
+      boolean enableStringSwitchConversion, TestParameters parameters) {
+    this.enableStringSwitchConversion = enableStringSwitchConversion;
     this.parameters = parameters;
   }
 
@@ -38,30 +45,35 @@
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
             options -> {
-              // TODO(b/135721688): Once a backend is in place for StringSwitch instructions,
-              //  generalize switch case removal for IntSwitch instructions to Switch instructions.
-              assert !options.enableStringSwitchConversion;
+              options.enableStringSwitchConversion = enableStringSwitchConversion;
+              assertTrue(options.minimumStringSwitchSize >= 3);
+              options.minimumStringSwitchSize = 2;
             })
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("FOO")
-        .inspect(codeInspector -> {
-          ClassSubject main = codeInspector.clazz(TestClass.class);
-          assertThat(main, isPresent());
-          MethodSubject mainMethod = main.mainMethod();
-          assertThat(mainMethod, isPresent());
-          assertEquals(0, countCall(mainMethod, "String", "hashCode"));
-          // Only "FOO" left
-          assertEquals(
-              1,
-              mainMethod.streamInstructions()
-                  .filter(i -> i.isConstString(JumboStringMode.ALLOW)).count());
-          // No branching points.
-          assertEquals(
-              0,
-              mainMethod.streamInstructions()
-                  .filter(i -> i.isIf() || i.isIfEqz() || i.isIfNez()).count());
-        });
+        .inspect(
+            codeInspector -> {
+              ClassSubject main = codeInspector.clazz(TestClass.class);
+              assertThat(main, isPresent());
+              MethodSubject mainMethod = main.mainMethod();
+              assertThat(mainMethod, isPresent());
+              assertEquals(0, countCall(mainMethod, "String", "hashCode"));
+              // Only "FOO" left
+              assertEquals(
+                  1,
+                  mainMethod
+                      .streamInstructions()
+                      .filter(i -> i.isConstString(JumboStringMode.ALLOW))
+                      .count());
+              // No branching points.
+              assertEquals(
+                  0,
+                  mainMethod
+                      .streamInstructions()
+                      .filter(i -> i.isIf() || i.isIfEqz() || i.isIfNez())
+                      .count());
+            });
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithHashCollisionsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithHashCollisionsTest.java
new file mode 100644
index 0000000..d4b6f89
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithHashCollisionsTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.switches;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class StringSwitchWithHashCollisionsTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StringSwitchWithHashCollisionsTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assertEquals("Hello world!".hashCode(), "Hello worla~".hashCode());
+    assertEquals("Hello world!".hashCode(), "Hello worlb_".hashCode());
+    assertNotEquals("Hello world!".hashCode(), "_".hashCode());
+
+    testForR8(parameters.getBackend())
+        .addInnerClasses(StringSwitchWithHashCollisionsTest.class)
+        .addKeepMainRule(TestClass.class)
+        .addOptionsModification(options -> assertTrue(options.minimumStringSwitchSize >= 3))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(
+            parameters.getRuntime(),
+            TestClass.class,
+            "Hello world!",
+            "_",
+            "Hello worla~",
+            "_",
+            "Hello worlb_",
+            "_",
+            "DONE")
+        .assertSuccessWithOutputLines(
+            "Hello world!", "Hello world, ish!", "Hello world, ish2!", "DONE");
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      for (String string : args) {
+        switch (string) {
+          case "Hello world!":
+            System.out.print("Hello world!");
+            break;
+
+          case "Hello worla~":
+            System.out.print("Hello world, ish!");
+            break;
+
+          case "Hello worlb_":
+            System.out.print("Hello world, ish2!");
+            break;
+
+          case "_":
+            System.out.println();
+            break;
+
+          default:
+            System.out.println("DONE");
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithSameTargetTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithSameTargetTest.java
index 1de3022..f035481 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithSameTargetTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWithSameTargetTest.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.switches;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -19,7 +21,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public StringSwitchWithSameTargetTest(TestParameters parameters) {
@@ -33,10 +35,10 @@
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
             options -> {
-              assert !options.enableStringSwitchConversion; // Remove once default.
-              options.enableStringSwitchConversion = true;
+              assertTrue(options.minimumStringSwitchSize >= 3);
+              options.minimumStringSwitchSize = 2;
             })
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class, "Hello", "_", "world!")
         .assertSuccessWithOutput("Hello world!");
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithMissingFieldTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithMissingFieldTest.java
new file mode 100644
index 0000000..796b3d9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithMissingFieldTest.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.switches;
+
+import static com.android.tools.r8.ToolHelper.getClassFilesForInnerClasses;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This tests that we gracefully handle the case where a switch map is incomplete, i.e., the
+ * corresponding enum has fields that are not present in the enum switch map.
+ */
+@RunWith(Parameterized.class)
+public class SwitchMapWithMissingFieldTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SwitchMapWithMissingFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramFiles(getSwitchMapProgramFile())
+        .addProgramClassFileData(
+            transformer(CompleteEnum.class)
+                .setClassDescriptor(descriptor(IncompleteEnum.class))
+                .transform())
+        .addKeepMainRule(TestClass.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("B", "D");
+  }
+
+  private static ClassReference getSwitchMapClassReference() {
+    return Reference.classFromTypeName(SwitchMapWithMissingFieldTest.class.getTypeName() + "$1");
+  }
+
+  private static Path getSwitchMapProgramFile() throws IOException {
+    return getClassFilesForInnerClasses(SwitchMapWithMissingFieldTest.class).stream()
+        .filter(
+            file ->
+                file.toString().endsWith(getSwitchMapClassReference().getBinaryName() + ".class"))
+        .findFirst()
+        .get();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      for (IncompleteEnum value : IncompleteEnum.values()) {
+        switch (value) {
+          case B:
+            System.out.println("B");
+            break;
+
+          case D:
+            System.out.println("D");
+            break;
+        }
+      }
+    }
+  }
+
+  enum CompleteEnum {
+    A,
+    B,
+    C,
+    D,
+    E;
+  }
+
+  enum IncompleteEnum {
+    B,
+    D;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithUnexpectedFieldTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithUnexpectedFieldTest.java
new file mode 100644
index 0000000..0679ef6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapWithUnexpectedFieldTest.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.switches;
+
+import static com.android.tools.r8.ToolHelper.getClassFilesForInnerClasses;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.Reference;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * This tests that we gracefully handle the case where a switch map refers to enum fields that are
+ * not present in the enum definition.
+ *
+ * <p>This situation may happen due to separate compilation. For example, a project may depend on a
+ * library that has been compiled against version X of a given enum but bundle version Y of the
+ * enum.
+ *
+ * <p>See also b/154315490.
+ */
+@RunWith(Parameterized.class)
+public class SwitchMapWithUnexpectedFieldTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SwitchMapWithUnexpectedFieldTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class)
+        .addProgramFiles(getSwitchMapProgramFile())
+        .addProgramClassFileData(
+            transformer(IncompleteEnum.class)
+                .setClassDescriptor(descriptor(CompleteEnum.class))
+                .transform())
+        .addKeepMainRule(TestClass.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("B", "D");
+  }
+
+  private static ClassReference getSwitchMapClassReference() {
+    return Reference.classFromTypeName(SwitchMapWithUnexpectedFieldTest.class.getTypeName() + "$1");
+  }
+
+  private static Path getSwitchMapProgramFile() throws IOException {
+    return getClassFilesForInnerClasses(SwitchMapWithUnexpectedFieldTest.class).stream()
+        .filter(
+            file ->
+                file.toString().endsWith(getSwitchMapClassReference().getBinaryName() + ".class"))
+        .findFirst()
+        .get();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      for (CompleteEnum value : CompleteEnum.values()) {
+        switch (value) {
+          case A:
+            System.out.println("A");
+            break;
+
+          case B:
+            System.out.println("B");
+            break;
+
+          case C:
+            System.out.println("C");
+            break;
+
+          case D:
+            System.out.println("D");
+            break;
+
+          case E:
+            System.out.println("E");
+            break;
+        }
+      }
+    }
+  }
+
+  enum CompleteEnum {
+    A,
+    B,
+    C,
+    D,
+    E;
+  }
+
+  enum IncompleteEnum {
+    B,
+    D;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index e570b8c..bbc6c26 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -59,6 +60,11 @@
     }
 
     @Override
+    public Value insertConstStringInstruction(AppView<?> appView, IRCode code, DexString value) {
+      throw new Unimplemented();
+    }
+
+    @Override
     public void replaceCurrentInstructionWithConstInt(IRCode code, int value) {
       throw new Unimplemented();
     }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
new file mode 100644
index 0000000..c120f9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
@@ -0,0 +1,70 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin.metadata;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertNotNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewritePassThroughTest extends KotlinMetadataTestBase {
+
+  @Parameterized.Parameters(name = "{0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+  }
+
+  private final TestParameters parameters;
+
+  public MetadataRewritePassThroughTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testKotlinStdLib() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepAllClassesRule()
+        .addKeepRules("-keep class kotlin.Metadata")
+        .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+        .compile()
+        .inspect(this::inspect);
+  }
+
+  public void inspect(CodeInspector inspector) throws IOException, ExecutionException {
+    CodeInspector stdLibInspector = new CodeInspector(ToolHelper.getKotlinStdlibJar());
+    for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
+      ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
+      assertThat(r8Clazz, isPresent());
+      KotlinClassMetadata originalMetadata = clazzSubject.getKotlinClassMetadata();
+      if (originalMetadata == null) {
+        assertNull(r8Clazz.getKotlinClassMetadata());
+        continue;
+      }
+      assertNotNull(r8Clazz.getKotlinClassMetadata());
+      // TODO(b/152153136): Extend the test with assertions about metadata equality.
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java
index 63fe00f..e0d117f 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingDesugarLambdaTest.java
@@ -1,3 +1,6 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming.applymapping;
 
 import static com.android.tools.r8.Collectors.toSingle;
diff --git a/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonSwitchTest.java b/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonSwitchTest.java
index 38f1534..97a96cc 100644
--- a/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonSwitchTest.java
@@ -28,7 +28,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public ClassNameComparisonSwitchTest(TestParameters parameters) {
@@ -40,13 +40,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(ClassNameComparisonSwitchTest.class)
         .addKeepMainRule(TestClass.class)
-        .addOptionsModification(
-            options -> {
-              assert !options.enableStringSwitchConversion;
-              options.enableStringSwitchConversion = true;
-            })
         .enableInliningAnnotations()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyClassesHaveBeenMinified)
         .run(parameters.getRuntime(), TestClass.class)
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index c67c66c..7b0e410 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.transformers;
 
+import static com.android.tools.r8.references.Reference.classFromTypeName;
+import static com.android.tools.r8.utils.DescriptorUtils.getBinaryNameFromDescriptor;
+import static com.android.tools.r8.utils.StringUtils.replaceAll;
 import static org.objectweb.asm.Opcodes.ASM7;
 
 import com.android.tools.r8.TestRuntime.CfVm;
@@ -28,8 +31,10 @@
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
 
 public class ClassFileTransformer {
 
@@ -114,19 +119,21 @@
   // Transformer utilities.
 
   private final byte[] bytes;
+  private final ClassReference classReference;
   private final List<ClassTransformer> classTransformers = new ArrayList<>();
   private final List<MethodTransformer> methodTransformers = new ArrayList<>();
 
-  private ClassFileTransformer(byte[] bytes) {
+  private ClassFileTransformer(byte[] bytes, ClassReference classReference) {
     this.bytes = bytes;
+    this.classReference = classReference;
   }
 
-  public static ClassFileTransformer create(byte[] bytes) {
-    return new ClassFileTransformer(bytes);
+  public static ClassFileTransformer create(byte[] bytes, ClassReference classReference) {
+    return new ClassFileTransformer(bytes, classReference);
   }
 
   public static ClassFileTransformer create(Class<?> clazz) throws IOException {
-    return create(ToolHelper.getClassAsBytes(clazz));
+    return create(ToolHelper.getClassAsBytes(clazz), classFromTypeName(clazz.getTypeName()));
   }
 
   public byte[] transform() {
@@ -145,6 +152,10 @@
     return this;
   }
 
+  public ClassReference getClassReference() {
+    return classReference;
+  }
+
   /** Unconditionally replace the implements clause of a class. */
   public ClassFileTransformer setImplements(Class<?>... interfaces) {
     return addClassTransformer(
@@ -169,29 +180,52 @@
           }
         });
   }
-  
+
   /** Unconditionally replace the descriptor (ie, qualified name) of a class. */
-  public ClassFileTransformer setClassDescriptor(String descriptor) {
-    assert DescriptorUtils.isClassDescriptor(descriptor);
+  public ClassFileTransformer setClassDescriptor(String newDescriptor) {
+    assert DescriptorUtils.isClassDescriptor(newDescriptor);
+    String newBinaryName = getBinaryNameFromDescriptor(newDescriptor);
     return addClassTransformer(
-        new ClassTransformer() {
-          @Override
-          public void visit(
-              int version,
-              int access,
-              String ignoredName,
-              String signature,
-              String superName,
-              String[] interfaces) {
-            super.visit(
-                version,
-                access,
-                DescriptorUtils.getBinaryNameFromDescriptor(descriptor),
-                signature,
-                superName,
-                interfaces);
-          }
-        });
+            new ClassTransformer() {
+              @Override
+              public void visit(
+                  int version,
+                  int access,
+                  String binaryName,
+                  String signature,
+                  String superName,
+                  String[] interfaces) {
+                super.visit(version, access, newBinaryName, signature, superName, interfaces);
+              }
+
+              @Override
+              public FieldVisitor visitField(
+                  int access, String name, String descriptor, String signature, Object object) {
+                return super.visitField(
+                    access,
+                    name,
+                    replaceAll(descriptor, getClassReference().getDescriptor(), newDescriptor),
+                    signature,
+                    object);
+              }
+
+              @Override
+              public MethodVisitor visitMethod(
+                  int access,
+                  String name,
+                  String descriptor,
+                  String signature,
+                  String[] exceptions) {
+                return super.visitMethod(
+                    access,
+                    name,
+                    replaceAll(descriptor, getClassReference().getDescriptor(), newDescriptor),
+                    signature,
+                    exceptions);
+              }
+            })
+        .replaceClassDescriptorInMethodInstructions(
+            getClassReference().getDescriptor(), newDescriptor);
   }
 
   public ClassFileTransformer setVersion(int newVersion) {
@@ -425,6 +459,55 @@
     void apply(Label start, Label end, Label handler, String type);
   }
 
+  public ClassFileTransformer replaceClassDescriptorInMethodInstructions(
+      String oldDescriptor, String newDescriptor) {
+    return addMethodTransformer(
+        new MethodTransformer() {
+          @Override
+          public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
+            super.visitFieldInsn(
+                opcode,
+                rewriteASMInternalTypeName(owner),
+                name,
+                replaceAll(descriptor, oldDescriptor, newDescriptor));
+          }
+
+          @Override
+          public void visitLdcInsn(Object value) {
+            if (value instanceof Type) {
+              Type type = (Type) value;
+              super.visitLdcInsn(
+                  Type.getType(replaceAll(type.getDescriptor(), oldDescriptor, newDescriptor)));
+            } else {
+              super.visitLdcInsn(value);
+            }
+          }
+
+          @Override
+          public void visitMethodInsn(
+              int opcode, String owner, String name, String descriptor, boolean isInterface) {
+            super.visitMethodInsn(
+                opcode,
+                rewriteASMInternalTypeName(owner),
+                name,
+                replaceAll(descriptor, oldDescriptor, newDescriptor),
+                isInterface);
+          }
+
+          @Override
+          public void visitTypeInsn(int opcode, String type) {
+            super.visitTypeInsn(opcode, rewriteASMInternalTypeName(type));
+          }
+
+          private String rewriteASMInternalTypeName(String type) {
+            return Type.getType(
+                    replaceAll(
+                        Type.getObjectType(type).getDescriptor(), oldDescriptor, newDescriptor))
+                .getInternalName();
+          }
+        });
+  }
+
   public ClassFileTransformer transformMethodInsnInMethod(
       String methodName, MethodInsnTransform transform) {
     return addMethodTransformer(
diff --git a/third_party/android_jar/lib-v30.tar.gz.sha1 b/third_party/android_jar/lib-v30.tar.gz.sha1
new file mode 100644
index 0000000..700ad7c
--- /dev/null
+++ b/third_party/android_jar/lib-v30.tar.gz.sha1
@@ -0,0 +1 @@
+629e0bfb886d247246268130d946678fa1a4ca9e
\ No newline at end of file
diff --git a/tools/linux/dalvik-4.0.4.tar.gz.sha1 b/tools/linux/dalvik-4.0.4.tar.gz.sha1
index bb9ee2f..34da306 100644
--- a/tools/linux/dalvik-4.0.4.tar.gz.sha1
+++ b/tools/linux/dalvik-4.0.4.tar.gz.sha1
@@ -1 +1 @@
-3815b581f86d3fe6a1affcd72bccad03cfaf0caa
\ No newline at end of file
+a338dffed04d76d9be0fb8f1ad49e44225bff311
\ No newline at end of file
diff --git a/tools/linux/dalvik.tar.gz.sha1 b/tools/linux/dalvik.tar.gz.sha1
index 3aea456..95e199d 100644
--- a/tools/linux/dalvik.tar.gz.sha1
+++ b/tools/linux/dalvik.tar.gz.sha1
@@ -1 +1 @@
-8350b21e7d340f44929d40ec26f7610cd45513f7
\ No newline at end of file
+1bc1761c5f62e9e40046acfae9fcb2092bb63c12
\ No newline at end of file
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 502666f..acc70ed 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -277,13 +277,9 @@
   subprocess.check_call('g4 open %s' % file, shell=True)
 
 
-def g4_add(files):
-  subprocess.check_call(' '.join(['g4', 'add'] + files), shell=True)
-
-
-def g4_change(version, r8version):
+def g4_change(version):
   return subprocess.check_output(
-      'g4 change --desc "Update R8 to version %s %s\n"' % (version, r8version),
+      'g4 change --desc "Update R8 to version %s\n"' % (version),
       shell=True)
 
 
@@ -320,64 +316,29 @@
     google3_base = subprocess.check_output(
         ['p4', 'g4d', '-f', 'update-r8']).rstrip()
     third_party_r8 = os.path.join(google3_base, 'third_party', 'java', 'r8')
-
-    # Check if new version folder is already created
     today = datetime.date.today()
-    new_version='v%d%02d%02d' % (today.year, today.month, today.day)
-    new_version_path = os.path.join(third_party_r8, new_version)
-
-    if os.path.exists(new_version_path):
-      shutil.rmtree(new_version_path)
-
-    # Remove old version
-    old_versions = []
-    for name in os.listdir(third_party_r8):
-      if os.path.isdir(os.path.join(third_party_r8, name)):
-        old_versions.append(name)
-    old_versions.sort()
-
-    if len(old_versions) >= 2:
-      shutil.rmtree(os.path.join(third_party_r8, old_versions[0]))
-
-    # Take current version to copy from
-    old_version=old_versions[-1]
-
-    # Create new version
-    assert not os.path.exists(new_version_path)
-    os.mkdir(new_version_path)
-
     with utils.ChangedWorkingDirectory(third_party_r8):
-      g4_cp(old_version, new_version, 'BUILD')
-      g4_cp(old_version, new_version, 'LICENSE')
-      g4_cp(old_version, new_version, 'METADATA')
+      # download files
+      g4_open('full.jar')
+      g4_open('src.jar')
+      g4_open('lib.jar')
+      g4_open('lib.jar.map')
+      download_file(options.version, 'r8-full-exclude-deps.jar', 'full.jar')
+      download_file(options.version, 'r8-src.jar', 'src.jar')
+      download_file(options.version, 'r8lib-exclude-deps.jar', 'lib.jar')
+      download_file(
+          options.version, 'r8lib-exclude-deps.jar.map', 'lib.jar.map')
+      g4_open('METADATA')
+      sed(r'[1-9]\.[0-9]{1,2}\.[0-9]{1,3}-dev',
+          options.version,
+          os.path.join(third_party_r8, 'METADATA'))
+      sed(r'\{ year.*\}',
+          ('{ year: %i month: %i day: %i }'
+           % (today.year, today.month, today.day)),
+          os.path.join(third_party_r8, 'METADATA'))
 
-      with utils.ChangedWorkingDirectory(new_version_path):
-        # update METADATA
-        g4_open('METADATA')
-        sed(r'[1-9]\.[0-9]{1,2}\.[0-9]{1,3}-dev',
-            options.version,
-            os.path.join(new_version_path, 'METADATA'))
-        sed(r'\{ year.*\}',
-            ('{ year: %i month: %i day: %i }'
-             % (today.year, today.month, today.day))
-            , os.path.join(new_version_path, 'METADATA'))
 
-        # update BUILD (is not necessary from v20190923)
-        g4_open('BUILD')
-        sed(old_version, new_version, os.path.join(new_version_path, 'BUILD'))
-
-        # download files
-        download_file(options.version, 'r8-full-exclude-deps.jar', 'r8.jar')
-        download_file(options.version, 'r8-src.jar', 'r8-src.jar')
-        download_file(options.version, 'r8lib-exclude-deps.jar', 'r8lib.jar')
-        download_file(
-            options.version, 'r8lib-exclude-deps.jar.map', 'r8lib.jar.map')
-        g4_add(['r8.jar', 'r8-src.jar', 'r8lib.jar', 'r8lib.jar.map'])
-
-      subprocess.check_output('chmod u+w %s/*' % new_version, shell=True)
-
-      g4_open('BUILD')
-      sed(old_version, new_version, os.path.join(third_party_r8, 'BUILD'))
+      subprocess.check_output('chmod u+w *', shell=True)
 
     with utils.ChangedWorkingDirectory(google3_base):
       blaze_result = blaze_run('//third_party/java/r8:d8 -- --version')
@@ -385,7 +346,7 @@
       assert options.version in blaze_result
 
       if not options.no_upload:
-        return g4_change(new_version, options.version)
+        return g4_change(options.version)
 
   return release_google3
 
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index 9182539..6d7905e 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -12,6 +12,7 @@
 import os
 import optparse
 import shutil
+import signal
 import subprocess
 import sys
 import time
@@ -415,6 +416,9 @@
   })
 ]
 
+def signal_handler(signum, frame):
+  subprocess.call(['pkill', 'java'])
+  raise Exception('Got killed by %s' % signum)
 
 class EnsureNoGradleAlive(object):
  def __init__(self, active):
@@ -422,6 +426,8 @@
 
  def __enter__(self):
    if self.active:
+     # If we timeout and get a sigterm we should still kill all java
+     signal.signal(signal.SIGTERM, signal_handler)
      print 'Running with wrapper that will kill java after'
 
  def __exit__(self, *_):