Merge "Reland "From DexAnnotation to KotlinInfo   via kotlinx.metadata.jvm.KotlinClassMetadata""
diff --git a/build.gradle b/build.gradle
index 7720cf3..7f41caa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,10 @@
     '-Xep:MissingDefault:ERROR',
     '-Xep:MultipleTopLevelClasses:ERROR',
     '-Xep:NarrowingCompoundAssignment:ERROR',
-    '-Xep:BoxedPrimitiveConstructor:ERROR']
+    '-Xep:BoxedPrimitiveConstructor:ERROR',
+    '-Xep:LogicalAssignment:ERROR',
+    '-Xep:FloatCast:ERROR',
+    '-Xep:ReturnValueIgnored:ERROR']
 
 apply from: 'copyAdditionalJctfCommonFiles.gradle'
 
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
index b3d59fb..a6389da 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -12,15 +12,15 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
@@ -60,7 +60,7 @@
     assert isArchive(archive);
     origin = new PathOrigin(archive);
     try {
-      zipFile = new ZipFile(archive.toFile());
+      zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8);
     } catch (IOException e) {
       if (!Files.exists(archive)) {
         throw new NoSuchFileException(archive.toString());
@@ -72,7 +72,7 @@
     while (entries.hasMoreElements()) {
       ZipEntry entry = entries.nextElement();
       String name = entry.getName();
-      if (FileUtils.isClassFile(Paths.get(name)) && include.test(name)) {
+      if (ZipUtils.isClassFile(name) && include.test(name)) {
         descriptors.add(DescriptorUtils.guessTypeDescriptor(name));
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
index baac9bc..1d6e678 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -3,21 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
-
 import com.android.tools.r8.ProgramResource.Kind;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -38,16 +35,15 @@
   }
 
   public static boolean includeClassFileEntries(String entry) {
-    return isClassFile(Paths.get(entry));
+    return ZipUtils.isClassFile(entry);
   }
 
   public static boolean includeDexEntries(String entry) {
-    return isDexFile(Paths.get(entry));
+    return ZipUtils.isDexFile(entry);
   }
 
   public static boolean includeClassFileOrDexEntries(String entry) {
-    Path path = Paths.get(entry);
-    return isClassFile(path) || isDexFile(path);
+    return ZipUtils.isClassFile(entry) || ZipUtils.isDexFile(entry);
   }
 
   private final Origin origin;
@@ -60,7 +56,10 @@
 
   public static ArchiveProgramResourceProvider fromArchive(
       Path archive, Predicate<String> include) {
-    return fromSupplier(new PathOrigin(archive), () -> new ZipFile(archive.toFile()), include);
+    return fromSupplier(
+        new PathOrigin(archive),
+        () -> new ZipFile(archive.toFile(), StandardCharsets.UTF_8),
+        include);
   }
 
   public static ArchiveProgramResourceProvider fromSupplier(
@@ -93,14 +92,13 @@
         ZipEntry entry = entries.nextElement();
         try (InputStream stream = zipFile.getInputStream(entry)) {
           String name = entry.getName();
-          Path path = Paths.get(name);
-          Origin entryOrigin = new ArchiveEntryOrigin(entry.getName(), origin);
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
           if (include.test(name)) {
-            if (FileUtils.isDexFile(path)) {
+            if (ZipUtils.isDexFile(name)) {
               dexResources.add(
                   ProgramResource.fromBytes(
                       entryOrigin, Kind.DEX, ByteStreams.toByteArray(stream), null));
-            } else if (isClassFile(path)) {
+            } else if (ZipUtils.isClassFile(name)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
               classResources.add(
                   ProgramResource.fromBytes(
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index 9e5d36a..93c0126 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -3,16 +3,79 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
 
 public class D8CommandParser extends BaseCompilerCommandParser {
 
+  static class OrderedClassFileResourceProvider implements ClassFileResourceProvider {
+    static class Builder {
+      private final ImmutableList.Builder<ClassFileResourceProvider> builder =
+          ImmutableList.builder();
+      boolean empty = true;
+
+      OrderedClassFileResourceProvider build() {
+        return new OrderedClassFileResourceProvider(builder.build());
+      }
+
+      Builder addClassFileResourceProvider(ClassFileResourceProvider provider) {
+        builder.add(provider);
+        empty = false;
+        return this;
+      }
+
+      boolean isEmpty() {
+        return empty;
+      }
+    }
+
+    final List<ClassFileResourceProvider> providers;
+    final Set<String> descriptors = Sets.newHashSet();
+
+    private OrderedClassFileResourceProvider(ImmutableList<ClassFileResourceProvider> providers) {
+      this.providers = providers;
+      // Collect all descriptors that can be provided.
+      this.providers.forEach(provider -> this.descriptors.addAll(provider.getClassDescriptors()));
+    }
+
+    static Builder builder() {
+      return new Builder();
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return descriptors;
+    }
+
+    @Override
+    public ProgramResource getProgramResource(String descriptor) {
+      // Search the providers in order. Return the program resource from the first provider that
+      // can provide it.
+      for (ClassFileResourceProvider provider : providers) {
+        if (provider.getClassDescriptors().contains(descriptor)) {
+          return provider.getProgramResource(descriptor);
+        }
+      }
+      return null;
+    }
+  }
+
   public static void main(String[] args) throws CompilationFailedException {
     D8Command command = parse(args, Origin.root()).build();
     if (command.isPrintHelp()) {
@@ -76,6 +139,8 @@
     Path outputPath = null;
     OutputMode outputMode = null;
     boolean hasDefinedApiLevel = false;
+    OrderedClassFileResourceProvider.Builder classpathBuilder =
+        OrderedClassFileResourceProvider.builder();
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder);
     try {
       for (int i = 0; i < expandedArgs.length; i++) {
@@ -115,7 +180,22 @@
         } else if (arg.equals("--lib")) {
           builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
         } else if (arg.equals("--classpath")) {
-          builder.addClasspathFiles(Paths.get(expandedArgs[++i]));
+          Path file = Paths.get(expandedArgs[++i]);
+          try {
+            if (!Files.exists(file)) {
+              throw new NoSuchFileException(file.toString());
+            }
+            if (isArchive(file)) {
+              classpathBuilder.addClassFileResourceProvider(new ArchiveClassFileProvider(file));
+            } else if (Files.isDirectory(file)) {
+              classpathBuilder.addClassFileResourceProvider(
+                  DirectoryClassFileProvider.fromDirectory(file));
+            } else {
+              throw new CompilationError("Unsupported classpath file type", new PathOrigin(file));
+            }
+          } catch (IOException e) {
+            builder.error(new ExceptionDiagnostic(e, new PathOrigin(file)));
+          }
         } else if (arg.equals("--main-dex-list")) {
           builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
         } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
@@ -140,6 +220,9 @@
           builder.addProgramFiles(Paths.get(arg));
         }
       }
+      if (!classpathBuilder.isEmpty()) {
+        builder.addClasspathResourceProvider(classpathBuilder.build());
+      }
       if (compilationMode != null) {
         builder.setMode(compilationMode);
       }
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 9b1599f..7b17ec8 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -14,7 +14,6 @@
 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;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -33,6 +32,7 @@
       private Path proguardMapFile = null;
       private boolean useSmali = false;
       private boolean allInfo = false;
+      private boolean useIr;
 
       @Override
       Builder self() {
@@ -63,6 +63,11 @@
         return this;
       }
 
+      public Builder setUseIr(boolean useIr) {
+        this.useIr = useIr;
+        return this;
+      }
+
       @Override
       protected DisassembleCommand makeCommand() {
         // If printing versions ignore everything else.
@@ -74,24 +79,26 @@
             getOutputPath(),
             proguardMapFile == null ? null : StringResource.fromFile(proguardMapFile),
             allInfo,
-            useSmali);
+            useSmali,
+            useIr);
       }
     }
 
-    static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
-        "Usage: disasm [options] <input-files>",
-        " where <input-files> are dex files",
-        " and options are:",
-        "  --all                   # Include all information in disassembly.",
-        "  --smali                 # Disassemble using smali syntax.",
-        "  --pg-map <file>         # Proguard map <file> for mapping names.",
-        "  --output                # Specify a file or directory to write to.",
-        "  --version               # Print the version of r8.",
-        "  --help                  # Print this message."));
-
+    static final String USAGE_MESSAGE =
+        "Usage: disasm [options] <input-files>\n"
+            + " where <input-files> are dex files\n"
+            + " and options are:\n"
+            + "  --all                   # Include all information in disassembly.\n"
+            + "  --smali                 # Disassemble using smali syntax.\n"
+            + "  --ir                    # Print IR before and after optimization.\n"
+            + "  --pg-map <file>         # Proguard map <file> for mapping names.\n"
+            + "  --output                # Specify a file or directory to write to.\n"
+            + "  --version               # Print the version of r8.\n"
+            + "  --help                  # Print this message.";
 
     private final boolean allInfo;
     private final boolean useSmali;
+    private final boolean useIr;
 
     public static Builder builder() {
       return new Builder();
@@ -112,10 +119,12 @@
           builder.setPrintHelp(true);
         } else if (arg.equals("--version")) {
           builder.setPrintVersion(true);
-          } else if (arg.equals("--all")) {
+        } else if (arg.equals("--all")) {
           builder.setAllInfo(true);
         } else if (arg.equals("--smali")) {
           builder.setUseSmali(true);
+        } else if (arg.equals("--ir")) {
+          builder.setUseIr(true);
         } else if (arg.equals("--pg-map")) {
           builder.setProguardMapFile(Paths.get(args[++i]));
         } else if (arg.equals("--output")) {
@@ -132,13 +141,18 @@
     }
 
     private DisassembleCommand(
-        AndroidApp inputApp, Path outputPath, StringResource proguardMap,
-        boolean allInfo, boolean useSmali) {
+        AndroidApp inputApp,
+        Path outputPath,
+        StringResource proguardMap,
+        boolean allInfo,
+        boolean useSmali,
+        boolean useIr) {
       super(inputApp);
       this.outputPath = outputPath;
       this.proguardMap = proguardMap;
       this.allInfo = allInfo;
       this.useSmali = useSmali;
+      this.useIr = useIr;
     }
 
     private DisassembleCommand(boolean printHelp, boolean printVersion) {
@@ -147,6 +161,7 @@
       proguardMap = null;
       allInfo = false;
       useSmali = false;
+      useIr = false;
     }
 
     public Path getOutputPath() {
@@ -157,6 +172,10 @@
       return useSmali;
     }
 
+    public boolean useIr() {
+      return useIr;
+    }
+
     @Override
     InternalOptions getInternalOptions() {
       InternalOptions internal = new InternalOptions();
@@ -190,9 +209,10 @@
     try {
       DexApplication application =
           new ApplicationReader(app, options, timing).read(command.proguardMap, executor);
-      DexByteCodeWriter writer = command.useSmali()
-          ? new SmaliWriter(application, options)
-          : new AssemblyWriter(application, options, command.allInfo);
+      DexByteCodeWriter writer =
+          command.useSmali()
+              ? new SmaliWriter(application, options)
+              : new AssemblyWriter(application, options, command.allInfo, command.useIr());
       if (command.getOutputPath() != null) {
         writer.write(command.getOutputPath());
       } else {
diff --git a/src/main/java/com/android/tools/r8/JarSizeCompare.java b/src/main/java/com/android/tools/r8/JarSizeCompare.java
new file mode 100644
index 0000000..aa16b1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/JarSizeCompare.java
@@ -0,0 +1,482 @@
+// Copyright (c) 2018, 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;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ClassNamingForNameMapper;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * JarSizeCompare outputs the class, method, field sizes of the given JAR files. For each input, a
+ * ProGuard map can be passed that is used to resolve minified names.
+ *
+ * <p>By default, only shows methods where R8's DEX output is 5 or more instructions larger than
+ * ProGuard+D8's output. Pass {@code --threshold 0} to display all methods.
+ */
+public class JarSizeCompare {
+
+  private static final String USAGE =
+      "Arguments:\n"
+          + "    [--threshold <threshold>]\n"
+          + "    [--lib <lib.jar>]\n"
+          + "    --input <name1> <input1.jar> [<map1.txt>]\n"
+          + "    --input <name2> <input2.jar> [<map2.txt>] ...\n"
+          + "\n"
+          + "JarSizeCompare outputs the class, method, field sizes of the given JAR files.\n"
+          + "For each input, a ProGuard map can be passed that is used to resolve minified names.\n";
+
+  private static final ImmutableMap<String, String> R8_RELOCATIONS =
+      ImmutableMap.<String, String>builder()
+          .put("com.google.common", "com.android.tools.r8.com.google.common")
+          .put("com.google.gson", "com.android.tools.r8.com.google.gson")
+          .put("com.google.thirdparty", "com.android.tools.r8.com.google.thirdparty")
+          .put("joptsimple", "com.android.tools.r8.joptsimple")
+          .put("org.apache.commons", "com.android.tools.r8.org.apache.commons")
+          .put("org.objectweb.asm", "com.android.tools.r8.org.objectweb.asm")
+          .put("it.unimi.dsi.fastutil", "com.android.tools.r8.it.unimi.dsi.fastutil")
+          .build();
+
+  private final List<Path> libraries;
+  private final List<InputParameter> inputParameters;
+  private final int threshold;
+  private final InternalOptions options;
+  private int pgIndex;
+  private int r8Index;
+
+  private JarSizeCompare(
+      List<Path> libraries, List<InputParameter> inputParameters, int threshold) {
+    this.libraries = libraries;
+    this.inputParameters = inputParameters;
+    this.threshold = threshold;
+    options = new InternalOptions();
+    options.enableCfFrontend = true;
+  }
+
+  public void run() throws Exception {
+    List<String> names = new ArrayList<>();
+    List<InputApplication> inputApplicationList = new ArrayList<>();
+    Timing timing = new Timing("JarSizeCompare");
+    for (InputParameter inputParameter : inputParameters) {
+      AndroidApp inputApp = inputParameter.getInputApp(libraries);
+      DexApplication input = inputParameter.getReader(options, inputApp, timing);
+      AndroidAppConsumers appConsumer = new AndroidAppConsumers();
+      D8.run(
+          D8Command.builder(inputApp)
+              .setMinApiLevel(AndroidApiLevel.P.getLevel())
+              .setProgramConsumer(appConsumer.wrapDexIndexedConsumer(null))
+              .build());
+      DexApplication d8Input = inputParameter.getReader(options, appConsumer.build(), timing);
+      InputApplication inputApplication =
+          new InputApplication(input, translateClassNames(input, input.classes()));
+      InputApplication d8Classes =
+          new InputApplication(input, translateClassNames(input, d8Input.classes()));
+      names.add(inputParameter.name + "-input");
+      inputApplicationList.add(inputApplication);
+      names.add(inputParameter.name + "-d8");
+      inputApplicationList.add(d8Classes);
+    }
+    if (threshold != 0) {
+      pgIndex = names.indexOf("pg-d8");
+      r8Index = names.indexOf("r8-d8");
+    }
+    Map<String, InputClass[]> inputClasses = new HashMap<>();
+    for (int i = 0; i < names.size(); i++) {
+      InputApplication classes = inputApplicationList.get(i);
+      for (String className : classes.getClasses()) {
+        inputClasses.computeIfAbsent(className, k -> new InputClass[names.size()])[i] =
+            classes.getInputClass(className);
+      }
+    }
+    for (Entry<String, Map<String, InputClass[]>> library : byLibrary(inputClasses)) {
+      System.out.println("");
+      System.out.println(Strings.repeat("=", 100));
+      String commonPrefix = getCommonPrefix(library.getValue().keySet());
+      if (library.getKey().isEmpty()) {
+        System.out.println("PROGRAM (" + commonPrefix + ")");
+      } else {
+        System.out.println("LIBRARY: " + library.getKey() + " (" + commonPrefix + ")");
+      }
+      printLibrary(library.getValue(), commonPrefix);
+    }
+  }
+
+  private Map<String, DexProgramClass> translateClassNames(
+      DexApplication input, List<DexProgramClass> classes) {
+    Map<String, DexProgramClass> result = new HashMap<>();
+    ClassNameMapper classNameMapper = input.getProguardMap();
+    for (DexProgramClass programClass : classes) {
+      ClassNamingForNameMapper classNaming;
+      if (classNameMapper == null) {
+        classNaming = null;
+      } else {
+        classNaming = classNameMapper.getClassNaming(programClass.type);
+      }
+      String type =
+          classNaming == null ? programClass.type.toSourceString() : classNaming.originalName;
+      result.put(type, programClass);
+    }
+    return result;
+  }
+
+  private String getCommonPrefix(Set<String> classes) {
+    if (classes.size() <= 1) {
+      return "";
+    }
+    String commonPrefix = null;
+    for (String clazz : classes) {
+      if (clazz.equals("r8.GeneratedOutlineSupport")) {
+        continue;
+      }
+      if (commonPrefix == null) {
+        commonPrefix = clazz;
+      } else {
+        int i = 0;
+        while (i < clazz.length()
+            && i < commonPrefix.length()
+            && clazz.charAt(i) == commonPrefix.charAt(i)) {
+          i++;
+        }
+        commonPrefix = commonPrefix.substring(0, i);
+      }
+    }
+    return commonPrefix;
+  }
+
+  private void printLibrary(Map<String, InputClass[]> classMap, String commonPrefix) {
+    List<Entry<String, InputClass[]>> classes = new ArrayList<>(classMap.entrySet());
+    classes.sort(Comparator.comparing(Entry::getKey));
+    for (Entry<String, InputClass[]> clazz : classes) {
+      printClass(
+          clazz.getKey().substring(commonPrefix.length()), new ClassCompare(clazz.getValue()));
+    }
+  }
+
+  private void printClass(String name, ClassCompare inputClasses) {
+    List<MethodSignature> methods = getMethods(inputClasses);
+    List<FieldSignature> fields = getFields(inputClasses);
+    if (methods.isEmpty() && fields.isEmpty()) {
+      return;
+    }
+    System.out.println(name);
+    for (MethodSignature sig : methods) {
+      printSignature(getMethodString(sig), inputClasses.sizes(sig));
+    }
+    for (FieldSignature sig : fields) {
+      printSignature(getFieldString(sig), inputClasses.sizes(sig));
+    }
+  }
+
+  private String getMethodString(MethodSignature sig) {
+    StringBuilder builder = new StringBuilder().append('(');
+    for (int i = 0; i < sig.parameters.length; i++) {
+      builder.append(DescriptorUtils.javaTypeToShorty(sig.parameters[i]));
+    }
+    builder.append(')').append(DescriptorUtils.javaTypeToShorty(sig.type)).append(' ');
+    return builder.append(sig.name).toString();
+  }
+
+  private String getFieldString(FieldSignature sig) {
+    return DescriptorUtils.javaTypeToShorty(sig.type) + ' ' + sig.name;
+  }
+
+  private void printSignature(String key, int[] sizes) {
+    System.out.print(padItem(key));
+    for (int size : sizes) {
+      System.out.print(padValue(size));
+    }
+    System.out.print('\n');
+  }
+
+  private List<MethodSignature> getMethods(ClassCompare inputClasses) {
+    List<MethodSignature> methods = new ArrayList<>();
+    for (MethodSignature methodSignature : inputClasses.getMethods()) {
+      if (threshold == 0 || methodExceedsThreshold(inputClasses, methodSignature)) {
+        methods.add(methodSignature);
+      }
+    }
+    return methods;
+  }
+
+  private boolean methodExceedsThreshold(
+      ClassCompare inputClasses, MethodSignature methodSignature) {
+    assert threshold > 0;
+    assert pgIndex != r8Index;
+    int pgSize = inputClasses.size(methodSignature, pgIndex);
+    int r8Size = inputClasses.size(methodSignature, r8Index);
+    return pgSize != -1 && r8Size != -1 && pgSize + threshold <= r8Size;
+  }
+
+  private List<FieldSignature> getFields(ClassCompare inputClasses) {
+    return threshold == 0 ? inputClasses.getFields() : Collections.emptyList();
+  }
+
+  private String padItem(String s) {
+    return String.format("%-52s", s);
+  }
+
+  private String padValue(int v) {
+    return String.format("%8s", v == -1 ? "---" : v);
+  }
+
+  private List<Map.Entry<String, Map<String, InputClass[]>>> byLibrary(
+      Map<String, InputClass[]> inputClasses) {
+    Map<String, Map<String, InputClass[]>> byLibrary = new HashMap<>();
+    for (Entry<String, InputClass[]> entry : inputClasses.entrySet()) {
+      Map<String, InputClass[]> library =
+          byLibrary.computeIfAbsent(getLibraryName(entry.getKey()), k -> new HashMap<>());
+      library.put(entry.getKey(), entry.getValue());
+    }
+    List<Entry<String, Map<String, InputClass[]>>> list = new ArrayList<>(byLibrary.entrySet());
+    list.sort(Comparator.comparing(Entry::getKey));
+    return list;
+  }
+
+  private String getLibraryName(String className) {
+    for (Entry<String, String> relocation : R8_RELOCATIONS.entrySet()) {
+      if (className.startsWith(relocation.getValue())) {
+        return relocation.getKey();
+      }
+    }
+    return "";
+  }
+
+  static class InputParameter {
+
+    private final String name;
+    private final Path jar;
+    private final Path map;
+
+    InputParameter(String name, Path jar, Path map) {
+      this.name = name;
+      this.jar = jar;
+      this.map = map;
+    }
+
+    DexApplication getReader(InternalOptions options, AndroidApp inputApp, Timing timing)
+        throws Exception {
+      ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
+      return applicationReader.read(map == null ? null : StringResource.fromFile(map)).toDirect();
+    }
+
+    AndroidApp getInputApp(List<Path> libraries) throws Exception {
+      return AndroidApp.builder().addLibraryFiles(libraries).addProgramFiles(jar).build();
+    }
+  }
+
+  static class InputApplication {
+
+    private final DexApplication dexApplication;
+    private final Map<String, DexProgramClass> classMap;
+
+    private InputApplication(DexApplication dexApplication, Map<String, DexProgramClass> classMap) {
+      this.dexApplication = dexApplication;
+      this.classMap = classMap;
+    }
+
+    public Set<String> getClasses() {
+      return classMap.keySet();
+    }
+
+    private InputClass getInputClass(String type) {
+      DexProgramClass inputClass = classMap.get(type);
+      ClassNameMapper proguardMap = dexApplication.getProguardMap();
+      return new InputClass(inputClass, proguardMap);
+    }
+  }
+
+  static class InputClass {
+    private final DexProgramClass programClass;
+    private final ClassNameMapper proguardMap;
+
+    InputClass(DexClass dexClass, ClassNameMapper proguardMap) {
+      this.programClass = dexClass == null ? null : dexClass.asProgramClass();
+      this.proguardMap = proguardMap;
+    }
+
+    void forEachMethod(BiConsumer<MethodSignature, DexEncodedMethod> consumer) {
+      if (programClass == null) {
+        return;
+      }
+      programClass.forEachMethod(
+          dexEncodedMethod -> {
+            MethodSignature originalSignature =
+                proguardMap == null
+                    ? null
+                    : ((MethodSignature) proguardMap.originalSignatureOf(dexEncodedMethod.method));
+            MethodSignature signature = MethodSignature.fromDexMethod(dexEncodedMethod.method);
+            consumer.accept(
+                originalSignature == null ? signature : originalSignature, dexEncodedMethod);
+          });
+    }
+
+    void forEachField(BiConsumer<FieldSignature, DexEncodedField> consumer) {
+      if (programClass == null) {
+        return;
+      }
+      programClass.forEachField(
+          dexEncodedField -> {
+            FieldSignature originalSignature =
+                proguardMap == null ? null : proguardMap.originalSignatureOf(dexEncodedField.field);
+            FieldSignature signature = FieldSignature.fromDexField(dexEncodedField.field);
+            consumer.accept(
+                originalSignature == null ? signature : originalSignature, dexEncodedField);
+          });
+    }
+  }
+
+  private static class ClassCompare {
+    final Map<MethodSignature, DexEncodedMethod[]> methods = new HashMap<>();
+    final Map<FieldSignature, DexEncodedField[]> fields = new HashMap<>();
+    final int classes;
+
+    ClassCompare(InputClass[] inputs) {
+      for (int i = 0; i < inputs.length; i++) {
+        InputClass inputClass = inputs[i];
+        int finalI = i;
+        if (inputClass == null) {
+          continue;
+        }
+        inputClass.forEachMethod(
+            (sig, m) ->
+                methods.computeIfAbsent(sig, o -> new DexEncodedMethod[inputs.length])[finalI] = m);
+        inputClass.forEachField(
+            (sig, f) ->
+                fields.computeIfAbsent(sig, o -> new DexEncodedField[inputs.length])[finalI] = f);
+      }
+      classes = inputs.length;
+    }
+
+    List<MethodSignature> getMethods() {
+      List<MethodSignature> methods = new ArrayList<>(this.methods.keySet());
+      methods.sort(Comparator.comparing(MethodSignature::toString));
+      return methods;
+    }
+
+    List<FieldSignature> getFields() {
+      List<FieldSignature> fields = new ArrayList<>(this.fields.keySet());
+      fields.sort(Comparator.comparing(FieldSignature::toString));
+      return fields;
+    }
+
+    int size(MethodSignature method, int classIndex) {
+      DexEncodedMethod dexEncodedMethod = methods.get(method)[classIndex];
+      if (dexEncodedMethod == null) {
+        return -1;
+      }
+      Code code = dexEncodedMethod.getCode();
+      if (code == null) {
+        return 0;
+      }
+      if (code.isCfCode()) {
+        return code.asCfCode().getInstructions().size();
+      }
+      if (code.isDexCode()) {
+        return code.asDexCode().instructions.length;
+      }
+      throw new Unreachable();
+    }
+
+    int[] sizes(MethodSignature method) {
+      int[] result = new int[classes];
+      for (int i = 0; i < classes; i++) {
+        result[i] = size(method, i);
+      }
+      return result;
+    }
+
+    int size(FieldSignature field, int classIndex) {
+      return fields.get(field)[classIndex] == null ? -1 : 1;
+    }
+
+    int[] sizes(FieldSignature field) {
+      int[] result = new int[classes];
+      for (int i = 0; i < classes; i++) {
+        result[i] = size(field, i);
+      }
+      return result;
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    JarSizeCompare program = JarSizeCompare.parse(args);
+    if (program == null) {
+      System.out.println(USAGE);
+    } else {
+      program.run();
+    }
+  }
+
+  public static JarSizeCompare parse(String[] args) {
+    int i = 0;
+    int threshold = 0;
+    List<Path> libraries = new ArrayList<>();
+    List<InputParameter> inputs = new ArrayList<>();
+    Set<String> names = new HashSet<>();
+    while (i < args.length) {
+      if (args[i].equals("--threshold") && i + 1 < args.length) {
+        threshold = Integer.parseInt(args[i + 1]);
+        i += 2;
+      } else if (args[i].equals("--lib") && i + 1 < args.length) {
+        libraries.add(Paths.get(args[i + 1]));
+        i += 2;
+      } else if (args[i].equals("--input") && i + 2 < args.length) {
+        String name = args[i + 1];
+        Path jar = Paths.get(args[i + 2]);
+        Path map = null;
+        if (i + 3 < args.length && !args[i + 3].startsWith("-")) {
+          map = Paths.get(args[i + 3]);
+          i += 4;
+        } else {
+          i += 3;
+        }
+        inputs.add(new InputParameter(name, jar, map));
+        if (!names.add(name)) {
+          System.out.println("Duplicate name: " + name);
+          return null;
+        }
+      } else {
+        return null;
+      }
+    }
+    if (inputs.size() < 2) {
+      return null;
+    }
+    if (threshold != 0 && (!names.contains("r8") || !names.contains("pg"))) {
+      System.out.println(
+          "You must either specify names \"pg\" and \"r8\" for input files "
+              + "or use \"--threshold 0\".");
+      return null;
+    }
+    return new JarSizeCompare(libraries, inputs, threshold);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
new file mode 100644
index 0000000..782c91d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2018, 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;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * PrintSeeds prints the classes, interfaces, methods and fields selected by a given ProGuard
+ * configuration &lt;pg-conf.txt&gt; when compiling a given program &lt;r8.jar&gt; alongside a given
+ * library &lt;rt.jar&gt;.
+ *
+ * <p>The output format is identical to what is printed when {@code -printseeds} is specified in
+ * &lt;pg-conf.txt&gt;, but running PrintSeeds can be faster than running R8 with {@code
+ * -printseeds}. See also the {@link PrintUses} program in R8.
+ */
+public class PrintSeeds {
+
+  private static final String USAGE =
+      "Arguments: <rt.jar> <r8.jar> <pg-conf.txt>\n"
+          + "\n"
+          + "PrintSeeds prints the classes, interfaces, methods and fields selected by\n"
+          + "<pg-conf.txt> when compiling <r8.jar> alongside <rt.jar>.\n"
+          + "\n"
+          + "The output format is identical to what is printed when -printseeds is specified in\n"
+          + "<pg-conf.txt>, but running PrintSeeds can be faster than running R8 with \n"
+          + "-printseeds. See also the "
+          + PrintUses.class.getSimpleName()
+          + " program in R8.";
+
+  public static void main(String[] args) throws Exception {
+    if (args.length != 3) {
+      System.out.println(USAGE.replace("\n", System.lineSeparator()));
+      System.exit(1);
+    }
+    Path rtJar = Paths.get(args[0]);
+    Path r8Jar = Paths.get(args[1]);
+    Path pgConf = Paths.get(args[2]);
+    R8Command command =
+        R8Command.builder()
+            .addLibraryFiles(rtJar)
+            .addProgramFiles(r8Jar)
+            .addProguardConfigurationFiles(pgConf)
+            .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+            .build();
+    Set<String> descriptors = new ArchiveClassFileProvider(r8Jar).getClassDescriptors();
+    InternalOptions options = command.getInternalOptions();
+    ExecutorService executorService = ThreadUtils.getExecutorService(options);
+    ExceptionUtils.withR8CompilationHandler(
+        command.getReporter(),
+        () -> {
+          try {
+            run(command, descriptors, options, executorService);
+          } finally {
+            executorService.shutdown();
+          }
+        });
+  }
+
+  private static void run(
+      R8Command command, Set<String> descriptors, InternalOptions options, ExecutorService executor)
+      throws IOException {
+    Timing timing = new Timing("PrintSeeds");
+    try {
+      DexApplication application =
+          new ApplicationReader(command.getInputApp(), options, timing).read(executor).toDirect();
+      AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+      RootSet rootSet =
+          new RootSetBuilder(
+                  appInfo, application, options.proguardConfiguration.getRules(), options)
+              .run(executor);
+      Enqueuer enqueuer = new Enqueuer(appInfo, options, false);
+      appInfo = enqueuer.traceApplication(rootSet, executor, timing);
+      RootSetBuilder.writeSeeds(
+          appInfo.withLiveness(),
+          System.out,
+          type -> descriptors.contains(type.toDescriptorString()));
+    } catch (ExecutionException e) {
+      R8.unwrapExecutionException(e);
+      throw new AssertionError(e); // unwrapping method should have thrown
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
new file mode 100644
index 0000000..c762182
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -0,0 +1,323 @@
+// Copyright (c) 2018, 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;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfo.ResolutionResult;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * PrintUses prints the classes, interfaces, methods and fields used by a given program
+ * &lt;sample.jar&gt;, restricted to classes and interfaces in a given library &lt;r8.jar&gt; that
+ * are not in &lt;sample.jar&gt;.
+ *
+ * <p>The output is in the same format as what is printed when specifying {@code -printseeds} in a
+ * ProGuard configuration file. See also the {@link PrintSeeds} program in R8.
+ *
+ * <p>Note that this tool is not related to the {@code -printusage} option of ProGuard configuration
+ * files.
+ */
+public class PrintUses {
+
+  private static final String USAGE =
+      "Arguments: <rt.jar> <r8.jar> <sample.jar>\n"
+          + "\n"
+          + "PrintUses prints the classes, interfaces, methods and fields used by <sample.jar>,\n"
+          + "restricted to classes and interfaces in <r8.jar> that are not in <sample.jar>.\n"
+          + "<rt.jar> and <r8.jar> should point to libraries used by <sample.jar>.\n"
+          + "\n"
+          + "The output is in the same format as what is printed when specifying -printseeds in\n"
+          + "a ProGuard configuration file. See also the "
+          + PrintSeeds.class.getSimpleName()
+          + " program in R8.";
+
+  private final Set<String> descriptors;
+  private final PrintStream out;
+  private Set<DexType> types = Sets.newIdentityHashSet();
+  private Map<DexType, Set<DexMethod>> methods = Maps.newIdentityHashMap();
+  private Map<DexType, Set<DexField>> fields = Maps.newIdentityHashMap();
+  private final DexApplication application;
+  private final AppInfoWithSubtyping appInfo;
+  private int errors;
+
+  class UseCollector extends UseRegistry {
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      DexEncodedMethod target = appInfo.lookupVirtualTarget(method.holder, method);
+      if (target != null && target.method != method) {
+        addType(method.holder);
+        addMethod(target.method);
+      } else {
+        addMethod(method);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      addMethod(method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      DexEncodedMethod target = appInfo.lookupStaticTarget(method);
+      if (target != null && target.method != method) {
+        addType(method.holder);
+        addMethod(target.method);
+      } else {
+        addMethod(method);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return registerInvokeVirtual(method);
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      addMethod(method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      addType(type);
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      addType(type);
+      return false;
+    }
+
+    private void addType(DexType type) {
+      if (isTargetType(type) && types.add(type)) {
+        methods.put(type, Sets.newIdentityHashSet());
+        fields.put(type, Sets.newIdentityHashSet());
+      }
+    }
+
+    private boolean isTargetType(DexType type) {
+      return descriptors.contains(type.toDescriptorString());
+    }
+
+    private void addField(DexField field) {
+      addType(field.type);
+      addType(field.clazz);
+      Set<DexField> typeFields = fields.get(field.clazz);
+      if (typeFields != null) {
+        typeFields.add(field);
+      }
+    }
+
+    private void addMethod(DexMethod method) {
+      addType(method.holder);
+      for (DexType parameterType : method.proto.parameters.values) {
+        addType(parameterType);
+      }
+      addType(method.proto.returnType);
+      Set<DexMethod> typeMethods = methods.get(method.holder);
+      if (typeMethods != null) {
+        typeMethods.add(method);
+      }
+    }
+
+    private void registerField(DexEncodedField field) {
+      registerTypeReference(field.field.type);
+    }
+
+    private void registerMethod(DexEncodedMethod method) {
+      DexEncodedMethod superTarget = appInfo.lookupSuperTarget(method.method, method.method.holder);
+      if (superTarget != null) {
+        registerInvokeSuper(superTarget.method);
+      }
+      for (DexType type : method.method.proto.parameters.values) {
+        registerTypeReference(type);
+      }
+      registerTypeReference(method.method.proto.returnType);
+      method.registerCodeReferences(this);
+    }
+
+    private void registerSuperType(DexProgramClass clazz, DexType superType) {
+      registerTypeReference(superType);
+      // If clazz overrides any methods in superType, we should keep those as well.
+      clazz.forEachMethod(
+          method -> {
+            ResolutionResult resolutionResult = appInfo.resolveMethod(superType, method.method);
+            for (DexEncodedMethod dexEncodedMethod : resolutionResult.asListOfTargets()) {
+              addMethod(dexEncodedMethod.method);
+            }
+          });
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    if (args.length != 3) {
+      System.out.println(USAGE.replace("\n", System.lineSeparator()));
+      return;
+    }
+    AndroidApp.Builder builder = AndroidApp.builder();
+    Path rtJar = Paths.get(args[0]);
+    builder.addLibraryFile(rtJar);
+    Path r8Jar = Paths.get(args[1]);
+    builder.addLibraryFile(r8Jar);
+    Path sampleJar = Paths.get(args[2]);
+    builder.addProgramFile(sampleJar);
+    Set<String> descriptors = new HashSet<>(getDescriptors(r8Jar));
+    descriptors.removeAll(getDescriptors(sampleJar));
+    PrintUses printUses = new PrintUses(descriptors, builder.build(), System.out);
+    printUses.analyze();
+    printUses.print();
+    if (printUses.errors > 0) {
+      System.err.println(printUses.errors + " errors");
+      System.exit(1);
+    }
+  }
+
+  private static Set<String> getDescriptors(Path path) throws IOException {
+    return new ArchiveClassFileProvider(path).getClassDescriptors();
+  }
+
+  private PrintUses(Set<String> descriptors, AndroidApp inputApp, PrintStream out)
+      throws Exception {
+    this.descriptors = descriptors;
+    this.out = out;
+    InternalOptions options = new InternalOptions();
+    application =
+        new ApplicationReader(inputApp, options, new Timing("PrintUses")).read().toDirect();
+    appInfo = new AppInfoWithSubtyping(application);
+  }
+
+  private void analyze() {
+    UseCollector useCollector = new UseCollector();
+    for (DexProgramClass dexProgramClass : application.classes()) {
+      useCollector.registerSuperType(dexProgramClass, dexProgramClass.superType);
+      for (DexType implementsType : dexProgramClass.interfaces.values) {
+        useCollector.registerSuperType(dexProgramClass, implementsType);
+      }
+      dexProgramClass.forEachMethod(useCollector::registerMethod);
+      dexProgramClass.forEachField(useCollector::registerField);
+    }
+  }
+
+  private void print() {
+    List<DexType> types = new ArrayList<>(this.types);
+    types.sort(Comparator.comparing(DexType::toSourceString));
+    for (DexType type : types) {
+      String typeName = type.toSourceString();
+      DexClass dexClass = application.definitionFor(type);
+      if (dexClass == null) {
+        error("Could not find definition for type " + type.toSourceString());
+        continue;
+      }
+      out.println(typeName);
+      List<DexMethod> methods = new ArrayList<>(this.methods.get(type));
+      List<String> methodDefinitions = new ArrayList<>(methods.size());
+      for (DexMethod method : methods) {
+        DexEncodedMethod encodedMethod = dexClass.lookupMethod(method);
+        if (encodedMethod == null) {
+          error("Could not find definition for method " + method.toSourceString());
+          continue;
+        }
+        methodDefinitions.add(getMethodSourceString(encodedMethod));
+      }
+      methodDefinitions.sort(Comparator.naturalOrder());
+      for (String encodedMethod : methodDefinitions) {
+        out.println(typeName + ": " + encodedMethod);
+      }
+      List<DexField> fields = new ArrayList<>(this.fields.get(type));
+      fields.sort(Comparator.comparing(DexField::toSourceString));
+      for (DexField field : fields) {
+        out.println(
+            typeName + ": " + field.type.toSourceString() + " " + field.name.toSourceString());
+      }
+    }
+  }
+
+  private void error(String message) {
+    out.println("# Error: " + message);
+    errors += 1;
+  }
+
+  private static String getMethodSourceString(DexEncodedMethod encodedMethod) {
+    DexMethod method = encodedMethod.method;
+    StringBuilder builder = new StringBuilder();
+    if (encodedMethod.accessFlags.isConstructor()) {
+      if (encodedMethod.accessFlags.isStatic()) {
+        builder.append("<clinit>");
+      } else {
+        String holderName = method.holder.toSourceString();
+        String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
+        builder.append(constructorName);
+      }
+    } else {
+      builder
+          .append(method.proto.returnType.toSourceString())
+          .append(" ")
+          .append(method.name.toSourceString());
+    }
+    builder.append("(");
+    for (int i = 0; i < method.getArity(); i++) {
+      if (i != 0) {
+        builder.append(",");
+      }
+      builder.append(method.proto.parameters.values[i].toSourceString());
+    }
+    builder.append(")");
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 077e220..d587efe 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
-import com.android.tools.r8.graph.ClassAndMemberPublicizer;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -28,6 +27,7 @@
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.naming.SourceFileRewriter;
+import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
 import com.android.tools.r8.origin.CommandLineOrigin;
@@ -42,8 +42,8 @@
 import com.android.tools.r8.shaking.ReasonPrinter;
 import com.android.tools.r8.shaking.RootSetBuilder;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
-import com.android.tools.r8.shaking.SimpleClassMerger;
 import com.android.tools.r8.shaking.TreePruner;
+import com.android.tools.r8.shaking.VerticalClassMerger;
 import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
@@ -287,7 +287,7 @@
         if (options.proguardConfiguration.isPrintSeeds()) {
           ByteArrayOutputStream bytes = new ByteArrayOutputStream();
           PrintStream out = new PrintStream(bytes);
-          RootSetBuilder.writeSeeds(appInfo.withLiveness(), out);
+          RootSetBuilder.writeSeeds(appInfo.withLiveness(), out, type -> true);
           out.flush();
           proguardSeedsData = bytes.toString();
         }
@@ -335,8 +335,8 @@
         // Class merging requires inlining.
         if (options.enableClassMerging && options.enableInlining) {
           timing.begin("ClassMerger");
-          SimpleClassMerger classMerger = new SimpleClassMerger(application,
-              appInfo.withLiveness(), graphLense, timing);
+          VerticalClassMerger classMerger =
+              new VerticalClassMerger(application, appInfo.withLiveness(), graphLense, timing);
           graphLense = classMerger.run();
           timing.end();
 
@@ -344,11 +344,11 @@
               .prunedCopyFrom(application, classMerger.getRemovedClasses());
         }
         if (options.proguardConfiguration.hasApplyMappingFile()) {
-          SeedMapper seedMapper = SeedMapper.seedMapperFromFile(
-              options.proguardConfiguration.getApplyMappingFile());
+          SeedMapper seedMapper =
+              SeedMapper.seedMapperFromFile(options.proguardConfiguration.getApplyMappingFile());
           timing.begin("apply-mapping");
-          graphLense = new ProguardMapApplier(appInfo.withLiveness(), graphLense, seedMapper)
-              .run(timing);
+          graphLense =
+              new ProguardMapApplier(appInfo.withLiveness(), graphLense, seedMapper).run(timing);
           timing.end();
         }
         application = application.asDirect().rewrittenWithLense(graphLense);
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 0e35847..f54233b 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -28,38 +28,32 @@
       return;
     }
     switch (args[0]) {
-      case "r8":
-        R8.main(shift(args));
-        break;
-      case "d8":
-        D8.main(shift(args));
+      case "bisect":
+        Bisect.main(shift(args));
         break;
       case "compatdx":
         CompatDx.main(shift(args));
         break;
-      case "dexfilemerger":
-        DexFileMerger.main(shift(args));
-        break;
-      case "dexsplitter":
-        DexSplitter.main(shift(args));
-        break;
       case "compatproguard":
         CompatProguard.main(shift(args));
         break;
+      case "d8":
+        D8.main(shift(args));
+        break;
       case "d8logger":
         D8Logger.main(shift(args));
         break;
-      case "disasm":
-        Disassemble.main(shift(args));
-        break;
-      case "bisect":
-        Bisect.main(shift(args));
+      case "dexfilemerger":
+        DexFileMerger.main(shift(args));
         break;
       case "dexsegments":
         DexSegments.main(shift(args));
         break;
-      case "maindex":
-        GenerateMainDexList.main(shift(args));
+      case "dexsplitter":
+        DexSplitter.main(shift(args));
+        break;
+      case "disasm":
+        Disassemble.main(shift(args));
         break;
       case "extractmarker":
         ExtractMarker.main(shift(args));
@@ -67,6 +61,21 @@
       case "jardiff":
         JarDiff.main(shift(args));
         break;
+      case "jarsizecompare":
+        JarSizeCompare.main(shift(args));
+        break;
+      case "maindex":
+        GenerateMainDexList.main(shift(args));
+        break;
+      case "printseeds":
+        PrintSeeds.main(shift(args));
+        break;
+      case "printuses":
+        PrintUses.main(shift(args));
+        break;
+      case "r8":
+        R8.main(shift(args));
+        break;
       default:
         runDefault(args);
         break;
diff --git a/src/main/java/com/android/tools/r8/UsageInformationConsumer.java b/src/main/java/com/android/tools/r8/UsageInformationConsumer.java
deleted file mode 100644
index a572908..0000000
--- a/src/main/java/com/android/tools/r8/UsageInformationConsumer.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
-import com.android.tools.r8.utils.ExceptionDiagnostic;
-import com.android.tools.r8.utils.FileUtils;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.file.Path;
-
-/**
- * Interface for receiving usage feedback from R8.
- *
- * The data is in the format defined for Proguard's <code>-printusage</code> flag. The information
- * will be produced if a consumer is provided. A consumer is automatically setup when running R8
- * with the Proguard <code>-printusage</code> flag set.
- */
-public interface UsageInformationConsumer {
-
-  /**
-   * Callback to receive the usage-information data.
-   *
-   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
-   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
-   * then the compiler guaranties to exit with an error.
-   *
-   * @param data UTF-8 encoded usage information.
-   * @param handler Diagnostics handler for reporting.
-   */
-  void acceptUsageInformation(byte[] data, DiagnosticsHandler handler);
-
-  static EmptyConsumer emptyConsumer() {
-    return EmptyConsumer.EMPTY_CONSUMER;
-  }
-
-  /** Empty consumer to request usage information but ignore the result. */
-  class EmptyConsumer implements UsageInformationConsumer {
-
-    private static final EmptyConsumer EMPTY_CONSUMER = new EmptyConsumer();
-
-    private EmptyConsumer() {}
-
-    @Override
-    public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
-      // Ignore content.
-    }
-  }
-
-    /** Forwarding consumer to delegate to an optional existing consumer. */
-  class ForwardingConsumer implements UsageInformationConsumer {
-
-    private final UsageInformationConsumer consumer;
-
-    /** @param consumer Consumer to forward to, if null, nothing will be forwarded. */
-    public ForwardingConsumer(UsageInformationConsumer consumer) {
-      this.consumer = consumer;
-    }
-
-    @Override
-    public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
-      if (consumer != null) {
-        consumer.acceptUsageInformation(data, handler);
-      }
-    }
-  }
-
-  /** File consumer to write contents to a file-system file. */
-  class FileConsumer extends ForwardingConsumer {
-
-    private final Path outputPath;
-
-    /** Consumer that writes to {@param outputPath}. */
-    public FileConsumer(Path outputPath) {
-      this(outputPath, null);
-    }
-
-    /** Consumer that forwards to {@param consumer} and also writes to {@param outputPath}. */
-    public FileConsumer(Path outputPath, UsageInformationConsumer consumer) {
-      super(consumer);
-      this.outputPath = outputPath;
-    }
-
-    @Override
-    public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
-      super.acceptUsageInformation(data, handler);
-      try {
-        FileUtils.writeToFile(outputPath, null, data);
-      } catch (IOException e) {
-        Origin origin = new PathOrigin(outputPath);
-        handler.error(new ExceptionDiagnostic(e, origin));
-      }
-    }
-  }
-
-  /**
-   * Stream consumer to write contents to an output stream.
-   *
-   * <p>Note: No close events are given to this stream so it should either be a permanent stream or
-   * the closing needs to happen outside of the compilation itself. If the stream is not one of the
-   * standard streams, i.e., System.out or System.err, you should likely implement yor own consumer.
-   */
-  class StreamConsumer extends ForwardingConsumer {
-
-    private final Origin origin;
-    private final OutputStream outputStream;
-
-    /** Consumer that writes to {@param outputStream}. */
-    public StreamConsumer(Origin origin, OutputStream outputStream) {
-      this(origin, outputStream, null);
-    }
-
-    /** Consumer that forwards to {@param consumer} and also writes to {@param outputStream}. */
-    public StreamConsumer(
-        Origin origin, OutputStream outputStream, UsageInformationConsumer consumer) {
-      super(consumer);
-      this.origin = origin;
-      this.outputStream = outputStream;
-    }
-
-    @Override
-    public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
-      super.acceptUsageInformation(data, handler);
-      try {
-        outputStream.write(data);
-      } catch (IOException e) {
-        handler.error(new ExceptionDiagnostic(e, origin));
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 0ba40b3..b775fd3 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.2.20-dev";
+  public static final String LABEL = "1.3.0-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java b/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
index 1336f95..807509a 100644
--- a/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
+++ b/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
@@ -22,7 +22,6 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 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.ZipUtils;
 import com.google.common.collect.ImmutableMap;
@@ -61,7 +60,7 @@
           archive.toString(),
           (entry, stream) -> {
             String name = entry.getName();
-            if (FileUtils.isClassFile(Paths.get(name))) {
+            if (ZipUtils.isClassFile(name)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
               builder.put(descriptor, ByteStreams.toByteArray(stream));
             }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index 015b8a3..0eed4a6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.CfSourceCode;
@@ -52,6 +53,10 @@
 
   @Override
   public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    if (!builder.isGeneratingClassFiles()) {
+      // TODO(b/109789539): Implement this case (see JarSourceCode.buildPrelude()/buildPostlude()).
+      throw new Unimplemented("CfMultiANewArray to DEX backend");
+    }
     int[] dimensions = state.popReverse(this.dimensions);
     builder.addMultiNewArray(type, state.push(type).register, dimensions);
   }
diff --git a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
index 90b863c..2621979 100644
--- a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
+++ b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -127,7 +128,7 @@
 
       List<ZipEntry> toDex = new ArrayList<>();
 
-      try (ZipFile zipFile = new ZipFile(input)) {
+      try (ZipFile zipFile = new ZipFile(input, StandardCharsets.UTF_8)) {
         final Enumeration<? extends ZipEntry> entries = zipFile.entries();
         while (entries.hasMoreElements()) {
           ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index f512a34..7d2907a 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -3,13 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.compatdx;
 
-import static com.android.tools.r8.utils.FileUtils.isApkFile;
-import static com.android.tools.r8.utils.FileUtils.isArchive;
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
-import static com.android.tools.r8.utils.FileUtils.isJarFile;
-import static com.android.tools.r8.utils.FileUtils.isZipFile;
-
 import com.android.tools.r8.CompatDxHelper;
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
@@ -28,12 +21,14 @@
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -474,7 +469,7 @@
     }
     if (singleDexFile) {
       return new SingleDexFileConsumer(
-          isDexFile(output)
+          FileUtils.isDexFile(output)
               ? new NamedDexFileConsumer(output)
               : createDexConsumer(output, inputs, keepClasses));
     }
@@ -485,13 +480,13 @@
       Path output, List<Path> inputs, boolean keepClasses)
       throws DxUsageMessage {
     if (keepClasses) {
-      if (!isArchive(output)) {
+      if (!FileUtils.isArchive(output)) {
         throw new DxCompatOptions.DxUsageMessage(
             "Output must be an archive when --keep-classes is set.");
       }
       return new DexKeepClassesConsumer(output, inputs);
     }
-    return isArchive(output)
+    return FileUtils.isArchive(output)
         ? new DexIndexedConsumer.ArchiveConsumer(output)
         : new DexIndexedConsumer.DirectoryConsumer(output);
   }
@@ -571,12 +566,12 @@
     private void writeZipWithClasses(DiagnosticsHandler handler) throws IOException {
       // For each input archive file, add all class files within.
       for (Path input : inputs) {
-        if (isArchive(input)) {
-          try (ZipFile zipFile = new ZipFile(input.toFile())) {
+        if (FileUtils.isArchive(input)) {
+          try (ZipFile zipFile = new ZipFile(input.toFile(), StandardCharsets.UTF_8)) {
             final Enumeration<? extends ZipEntry> entries = zipFile.entries();
             while (entries.hasMoreElements()) {
               ZipEntry entry = entries.nextElement();
-              if (isClassFile(Paths.get(entry.getName()))) {
+              if (ZipUtils.isClassFile(entry.getName())) {
                 try (InputStream entryStream = zipFile.getInputStream(entry)) {
                   outputBuilder.addFile(
                       entry.getName(), ByteStreams.toByteArray(entryStream), handler);
@@ -603,11 +598,11 @@
       return;
     }
     Path path = file.toPath();
-    if (isZipFile(path) || isJarFile(path) || isClassFile(path)) {
+    if (FileUtils.isZipFile(path) || FileUtils.isJarFile(path) || FileUtils.isClassFile(path)) {
       files.add(path);
       return;
     }
-    if (isApkFile(path)) {
+    if (FileUtils.isApkFile(path)) {
       throw new Unimplemented("apk files not yet supported");
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
index 89abadd..23dbfbf 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -3,9 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.conversion.OptimizationFeedbackIgnore;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
 import java.io.PrintStream;
 
 public class AssemblyWriter extends DexByteCodeWriter {
@@ -13,12 +19,29 @@
   private final boolean writeAllClassInfo;
   private final boolean writeFields;
   private final boolean writeAnnotations;
+  private final boolean writeIR;
+  private final AppInfoWithSubtyping appInfo;
+  private final Timing timing = new Timing("AssemblyWriter");
+  private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
 
-  public AssemblyWriter(DexApplication application, InternalOptions options, boolean allInfo) {
+  public AssemblyWriter(
+      DexApplication application, InternalOptions options, boolean allInfo, boolean writeIR) {
     super(application, options);
     this.writeAllClassInfo = allInfo;
     this.writeFields = allInfo;
     this.writeAnnotations = allInfo;
+    this.writeIR = writeIR;
+    if (writeIR) {
+      this.appInfo = new AppInfoWithSubtyping(application.toDirect());
+      if (options.programConsumer == null) {
+        // Use class-file backend, since the CF frontend for testing does not support desugaring of
+        // synchronized methods for the DEX backend (b/109789541).
+        options.programConsumer = ClassFileConsumer.emptyConsumer();
+      }
+      options.outline.enabled = false;
+    } else {
+      this.appInfo = null;
+    }
   }
 
   @Override
@@ -88,10 +111,21 @@
     ps.println();
     Code code = method.getCode();
     if (code != null) {
-      ps.println(code.toString(method, naming));
+      if (writeIR) {
+        writeIR(method, ps);
+      } else {
+        ps.println(code.toString(method, naming));
+      }
     }
   }
 
+  private void writeIR(DexEncodedMethod method, PrintStream ps) {
+    CfgPrinter printer = new CfgPrinter();
+    new IRConverter(appInfo, options, timing, printer)
+        .processMethod(method, ignoreOptimizationFeedback, null, null, null);
+    ps.println(printer.toString());
+  }
+
   private void writeAnnotations(DexAnnotationSet annotations, PrintStream ps) {
     if (writeAnnotations) {
       if (!annotations.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index ffa34d1..b8e0bd8 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -225,8 +225,11 @@
       ValueNumberGenerator generator,
       Position callerPosition,
       Origin origin) {
-    assert !options.isGeneratingDex() || !encodedMethod.accessFlags.isSynchronized()
-        : "Converting CfCode to IR not supported for DEX output of synchronized methods.";
+    // TODO(b/109789541): Implement CF->IR->DEX for synchronized methods.
+    if (options.isGeneratingDex() && encodedMethod.accessFlags.isSynchronized()) {
+      throw new Unimplemented(
+          "Converting CfCode to IR not supported for DEX output of synchronized methods.");
+    }
     CfSourceCode source = new CfSourceCode(this, encodedMethod, callerPosition, origin);
     IRBuilder builder =
         (generator == null)
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
new file mode 100644
index 0000000..7f31098
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+public class DefaultUseRegistry extends UseRegistry {
+
+  @Override
+  public boolean registerInvokeVirtual(DexMethod method) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInvokeDirect(DexMethod method) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInvokeStatic(DexMethod method) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInvokeInterface(DexMethod method) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInvokeSuper(DexMethod method) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInstanceFieldWrite(DexField field) {
+    return true;
+  }
+
+  @Override
+  public boolean registerInstanceFieldRead(DexField field) {
+    return true;
+  }
+
+  @Override
+  public boolean registerNewInstance(DexType type) {
+    return true;
+  }
+
+  @Override
+  public boolean registerStaticFieldRead(DexField field) {
+    return true;
+  }
+
+  @Override
+  public boolean registerStaticFieldWrite(DexField field) {
+    return true;
+  }
+
+  @Override
+  public boolean registerTypeReference(DexType type) {
+    return true;
+  }
+}
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 23139c5..2c188da 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Iterators;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
@@ -96,6 +97,16 @@
     }
   }
 
+  public Iterable<DexEncodedField> fields() {
+    return () ->
+        Iterators.concat(Iterators.forArray(instanceFields), Iterators.forArray(staticFields));
+  }
+
+  public Iterable<DexEncodedMethod> methods() {
+    return () ->
+        Iterators.concat(Iterators.forArray(directMethods), Iterators.forArray(virtualMethods));
+  }
+
   @Override
   void collectMixedSectionItems(MixedSectionCollection mixedItems) {
     throw new Unreachable();
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 831edbe..026c2b5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -790,6 +790,7 @@
     private Code code;
     private CompilationState compilationState = CompilationState.NOT_PROCESSED;
     private OptimizationInfo optimizationInfo = DefaultOptimizationInfo.DEFAULT;
+    private final int classFileVersion;
 
     private Builder(DexEncodedMethod from) {
       // Copy all the mutable state of a DexEncodedMethod here.
@@ -800,6 +801,7 @@
       code = from.code;
       compilationState = from.compilationState;
       optimizationInfo = from.optimizationInfo.copy();
+      classFileVersion = from.classFileVersion;
     }
 
     public void setMethod(DexMethod method) {
@@ -816,7 +818,8 @@
       assert annotations != null;
       assert parameterAnnotations != null;
       DexEncodedMethod result =
-          new DexEncodedMethod(method, accessFlags, annotations, parameterAnnotations, code);
+          new DexEncodedMethod(
+              method, accessFlags, annotations, parameterAnnotations, code, classFileVersion);
       result.compilationState = compilationState;
       result.optimizationInfo = optimizationInfo;
       return result;
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index 06ea28d..cd3c093 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Opcodes;
@@ -159,6 +160,26 @@
     public boolean isInvokeConstructor() {
       return this == MethodHandleType.INVOKE_CONSTRUCTOR;
     }
+
+    public Type toInvokeType() {
+      assert isMethodType();
+      switch (this) {
+        case INVOKE_STATIC:
+          return Type.STATIC;
+        case INVOKE_INSTANCE:
+          return Type.VIRTUAL;
+        case INVOKE_CONSTRUCTOR:
+          return Type.DIRECT;
+        case INVOKE_DIRECT:
+          return Type.DIRECT;
+        case INVOKE_INTERFACE:
+          return Type.INTERFACE;
+        case INVOKE_SUPER:
+          return Type.SUPER;
+        default:
+          throw new Unreachable("DexMethodHandle with unexpected type: " + this);
+      }
+    }
   }
 
   public MethodHandleType type;
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index ed99d25..2310891 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -557,7 +557,7 @@
 
     @Override
     public int hashCode() {
-      return (int) value * 19;
+      return (int) (value * 19);
     }
 
     @Override
@@ -615,7 +615,7 @@
 
     @Override
     public int hashCode() {
-      return (int) value * 29;
+      return (int) (value * 29);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index 8d5f0d0..e4f1c8c 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -69,7 +69,6 @@
   }
 
   public DirectMappedDexApplication rewrittenWithLense(GraphLense graphLense) {
-    assert graphLense.isContextFree();
     assert mappingIsValid(graphLense, programClasses.getAllTypes());
     assert mappingIsValid(graphLense, libraryClasses.keySet());
     // As a side effect, this will rebuild the program classes and library classes maps.
@@ -81,7 +80,7 @@
     // (e.g. relinking a type) or it might encode a type that was renamed, in which case the
     // original type will point to a definition that was renamed.
     for (DexType type : types) {
-      DexType renamed = graphLense.lookupType(type, null);
+      DexType renamed = graphLense.lookupType(type);
       if (renamed != type) {
         if (definitionFor(type).type != renamed && definitionFor(renamed) == null) {
           return false;
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index ed4106e..096de5a 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -3,8 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A GraphLense implements a virtual view on top of the graph, used to delay global rewrites until
@@ -59,13 +63,33 @@
     return new Builder();
   }
 
-  public abstract DexType lookupType(DexType type, DexEncodedMethod context);
+  public abstract DexType lookupType(DexType type);
 
-  public abstract DexMethod lookupMethod(DexMethod method, DexEncodedMethod context);
+  // This overload can be used when the graph lense is known to be context insensitive.
+  public DexMethod lookupMethod(DexMethod method) {
+    assert isContextFreeForMethod(method);
+    return lookupMethod(method, null, null);
+  }
 
-  public abstract DexField lookupField(DexField field, DexEncodedMethod context);
+  public abstract DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type);
 
-  public abstract boolean isContextFree();
+  // Context sensitive graph lenses should override this method.
+  public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+    assert isContextFreeForMethod(method);
+    DexMethod result = lookupMethod(method);
+    if (result != null) {
+      return ImmutableSet.of(result);
+    }
+    return ImmutableSet.of();
+  }
+
+  public abstract DexField lookupField(DexField field);
+
+  public abstract boolean isContextFreeForMethods();
+
+  public boolean isContextFreeForMethod(DexMethod method) {
+    return isContextFreeForMethods();
+  }
 
   public static GraphLense getIdentityLense() {
     return new IdentityGraphLense();
@@ -78,22 +102,22 @@
   private static class IdentityGraphLense extends GraphLense {
 
     @Override
-    public DexType lookupType(DexType type, DexEncodedMethod context) {
+    public DexType lookupType(DexType type) {
       return type;
     }
 
     @Override
-    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
+    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
       return method;
     }
 
     @Override
-    public DexField lookupField(DexField field, DexEncodedMethod context) {
+    public DexField lookupField(DexField field) {
       return field;
     }
 
     @Override
-    public boolean isContextFree() {
+    public boolean isContextFreeForMethods() {
       return true;
     }
   }
@@ -118,14 +142,14 @@
     }
 
     @Override
-    public DexType lookupType(DexType type, DexEncodedMethod context) {
+    public DexType lookupType(DexType type) {
       if (type.isArrayType()) {
         synchronized (this) {
           // This block need to be synchronized due to arrayTypeCache.
           DexType result = arrayTypeCache.get(type);
           if (result == null) {
             DexType baseType = type.toBaseType(dexItemFactory);
-            DexType newType = lookupType(baseType, context);
+            DexType newType = lookupType(baseType);
             if (baseType == newType) {
               result = type;
             } else {
@@ -136,25 +160,39 @@
           return result;
         }
       }
-      DexType previous = previousLense.lookupType(type, context);
+      DexType previous = previousLense.lookupType(type);
       return typeMap.getOrDefault(previous, previous);
     }
 
     @Override
-    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
-      DexMethod previous = previousLense.lookupMethod(method, context);
+    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+      DexMethod previous = previousLense.lookupMethod(method, context, type);
       return methodMap.getOrDefault(previous, previous);
     }
 
     @Override
-    public DexField lookupField(DexField field, DexEncodedMethod context) {
-      DexField previous = previousLense.lookupField(field, context);
+    public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+      Set<DexMethod> result = new HashSet<>();
+      for (DexMethod previous : previousLense.lookupMethodInAllContexts(method)) {
+        result.add(methodMap.getOrDefault(previous, previous));
+      }
+      return result;
+    }
+
+    @Override
+    public DexField lookupField(DexField field) {
+      DexField previous = previousLense.lookupField(field);
       return fieldMap.getOrDefault(previous, previous);
     }
 
     @Override
-    public boolean isContextFree() {
-      return previousLense.isContextFree();
+    public boolean isContextFreeForMethods() {
+      return previousLense.isContextFreeForMethods();
+    }
+
+    @Override
+    public boolean isContextFreeForMethod(DexMethod method) {
+      return previousLense.isContextFreeForMethod(method);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index 454ed29..afc2969 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -533,7 +533,8 @@
           return MemberType.OBJECT;
         case Opcodes.BALOAD:
         case Opcodes.BASTORE:
-          return MemberType.BOOLEAN; // TODO: Distinguish byte and boolean.
+          // TODO(b/109788783): Distinguish byte and boolean.
+          return MemberType.BOOLEAN;
         case Opcodes.CALOAD:
         case Opcodes.CASTORE:
           return MemberType.CHAR;
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 113d522..819c6f2 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
@@ -6,10 +6,11 @@
 import static com.android.tools.r8.ir.code.IRCode.INSTRUCTION_NUMBER_DELTA;
 
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DebugLocalInfo.PrintLevel;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
@@ -20,10 +21,13 @@
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -173,7 +177,7 @@
   public void removeSuccessor(BasicBlock block) {
     int index = successors.indexOf(block);
     assert index >= 0 : "removeSuccessor did not find the successor to remove";
-    removeSuccessorsByIndex(Collections.singletonList(index));
+    removeSuccessorsByIndex(new IntArrayList(new int[] {index}));
   }
 
   public void removePredecessor(BasicBlock block) {
@@ -328,7 +332,7 @@
     assert false : "replaceSuccessor did not find the predecessor to replace";
   }
 
-  public void removeSuccessorsByIndex(List<Integer> successorsToRemove) {
+  public void removeSuccessorsByIndex(IntList successorsToRemove) {
     if (successorsToRemove.isEmpty()) {
       return;
     }
@@ -463,7 +467,7 @@
         return instruction;
       }
     }
-    throw new Unreachable();
+    return null;
   }
 
   public void clearUserInfo() {
@@ -678,6 +682,44 @@
     catchHandlers = new CatchHandlers<>(guards, successorIndexes);
   }
 
+  // Due to class merging, it is possible that two exception classes have been merged into one.
+  // This function renames the guards according to the given graph lense.
+  public void renameGuardsInCatchHandlers(GraphLense graphLense) {
+    assert hasCatchHandlers();
+    List<DexType> newGuards = new ArrayList<>(catchHandlers.getGuards().size());
+    for (DexType guard : catchHandlers.getGuards()) {
+      // The type may have changed due to class merging.
+      newGuards.add(graphLense.lookupType(guard));
+    }
+    this.catchHandlers = new CatchHandlers<>(newGuards, catchHandlers.getAllTargets());
+  }
+
+  public boolean consistentCatchHandlers() {
+    // Check that catch handlers are always the first successors of a block.
+    if (hasCatchHandlers()) {
+      assert exit().isGoto() || exit().isThrow();
+      CatchHandlers<Integer> catchHandlers = getCatchHandlersWithSuccessorIndexes();
+      // If there is a catch-all guard it must be the last.
+      List<DexType> guards = catchHandlers.getGuards();
+      int lastGuardIndex = guards.size() - 1;
+      for (int i = 0; i < guards.size(); i++) {
+        assert guards.get(i) != DexItemFactory.catchAllType || i == lastGuardIndex;
+      }
+      // Check that all successors except maybe the last are catch successors.
+      List<Integer> sortedHandlerIndices = new ArrayList<>(catchHandlers.getAllTargets());
+      sortedHandlerIndices.sort(Comparator.naturalOrder());
+      int firstIndex = sortedHandlerIndices.get(0);
+      int lastIndex = sortedHandlerIndices.get(sortedHandlerIndices.size() - 1);
+      assert firstIndex == 0;
+      assert lastIndex < sortedHandlerIndices.size();
+      int lastSuccessorIndex = getSuccessors().size() - 1;
+      assert lastIndex == lastSuccessorIndex // All successors are catch successors.
+          || lastIndex == lastSuccessorIndex - 1; // All but one successors are catch successors.
+      assert lastIndex == lastSuccessorIndex || !exit().isThrow();
+    }
+    return true;
+  }
+
   public void clearCurrentDefinitions() {
     currentDefinitions = null;
     for (Phi phi : getPhis()) {
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 a5d3e4a..c832991 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
@@ -5,8 +5,6 @@
 
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
@@ -473,28 +471,7 @@
 
   private boolean consistentCatchHandlers() {
     for (BasicBlock block : blocks) {
-      // Check that catch handlers are always the first successors of a block.
-      if (block.hasCatchHandlers()) {
-        assert block.exit().isGoto() || block.exit().isThrow();
-        CatchHandlers<Integer> catchHandlers = block.getCatchHandlersWithSuccessorIndexes();
-        // If there is a catch-all guard it must be the last.
-        List<DexType> guards = catchHandlers.getGuards();
-        int lastGuardIndex = guards.size() - 1;
-        for (int i = 0; i < guards.size(); i++) {
-          assert guards.get(i) != DexItemFactory.catchAllType || i == lastGuardIndex;
-        }
-        // Check that all successors except maybe the last are catch successors.
-        List<Integer> sortedHandlerIndices = new ArrayList<>(catchHandlers.getAllTargets());
-        sortedHandlerIndices.sort(Comparator.naturalOrder());
-        int firstIndex = sortedHandlerIndices.get(0);
-        int lastIndex = sortedHandlerIndices.get(sortedHandlerIndices.size() - 1);
-        assert firstIndex == 0;
-        assert lastIndex < sortedHandlerIndices.size();
-        int lastSuccessorIndex = block.getSuccessors().size() - 1;
-        assert lastIndex == lastSuccessorIndex  // All successors are catch successors.
-            || lastIndex == lastSuccessorIndex - 1; // All but one successors are catch successors.
-        assert lastIndex == lastSuccessorIndex || !block.exit().isThrow();
-      }
+      assert block.consistentCatchHandlers();
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 8330e06..21b8dc4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -363,7 +363,7 @@
 
     private void processInvoke(Type type, DexMethod method) {
       DexEncodedMethod source = caller.method;
-      method = graphLense.lookupMethod(method, source);
+      method = graphLense.lookupMethod(method, source, type);
       DexEncodedMethod definition = appInfo.lookup(type, method, source.method.holder);
       if (definition != null) {
         assert !source.accessFlags.isBridge() || definition != caller.method;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index e798fd2..de86cb4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -118,13 +119,16 @@
   }
 
   public CfCode build(
-      CodeRewriter rewriter, InternalOptions options, AppInfoWithSubtyping appInfo) {
+      CodeRewriter rewriter,
+      GraphLense graphLense,
+      InternalOptions options,
+      AppInfoWithSubtyping appInfo) {
     computeInitializers();
     types = new TypeVerificationHelper(code, factory, appInfo).computeVerificationTypes();
     splitExceptionalBlocks();
     LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, types);
     loadStoreHelper.insertLoadsAndStores();
-    DeadCodeRemover.removeDeadCode(code, rewriter, options);
+    DeadCodeRemover.removeDeadCode(code, rewriter, graphLense, options);
     removeUnneededLoadsAndStores();
     registerAllocator = new CfRegisterAllocator(code, options);
     registerAllocator.allocateRegisters();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index e68e3d5..309e1d0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -324,7 +324,7 @@
     buildArgumentInstructions(builder);
     recordStateForTarget(0, state.getSnapshot());
     // TODO: addDebugLocalUninitialized + addDebugLocalStart for non-argument locals live at 0
-    // TODO: Generate method synchronization
+    // TODO(b/109789541): Generate method synchronization for DEX backend.
     inPrelude = false;
   }
 
@@ -353,7 +353,7 @@
 
   @Override
   public void buildPostlude(IRBuilder builder) {
-    // Since we're generating classfiles, we never need to synthesize monitor enter/exit.
+    // TODO(b/109789541): Generate method synchronization for DEX backend.
   }
 
   @Override
@@ -367,6 +367,13 @@
     }
     setLocalVariableLists();
     readEndingLocals(builder);
+    if (currentBlockInfo != null && instruction.canThrow()) {
+      Snapshot exceptionTransfer =
+          state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
+      for (int target : currentBlockInfo.exceptionalSuccessors) {
+        recordStateForTarget(target, exceptionTransfer);
+      }
+    }
     if (isControlFlow(instruction)) {
       ensureDebugValueLivenessControl(builder);
       instruction.buildIR(builder, state, this);
@@ -376,13 +383,6 @@
       }
       state.clear();
     } else {
-      if (currentBlockInfo != null && instruction.canThrow()) {
-        Snapshot exceptionTransfer =
-            state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
-        for (int target : currentBlockInfo.exceptionalSuccessors) {
-          recordStateForTarget(target, exceptionTransfer);
-        }
-      }
       instruction.buildIR(builder, state, this);
       ensureDebugValueLiveness(builder);
       if (builder.getCFG().containsKey(currentInstructionIndex + 1)) {
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 81dc436..7d7e5f3 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
@@ -46,6 +46,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -140,7 +141,7 @@
       this.memberValuePropagation =
           options.enableValuePropagation ?
               new MemberValuePropagation(appInfo.withLiveness()) : null;
-      this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping());
+      this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping(), options);
       if (appInfo.hasLiveness()) {
         // When disabling the pruner here, also disable the ProtoLiteExtension in R8.java.
         this.protoLiteRewriter =
@@ -482,7 +483,7 @@
                 // StringBuilder/StringBuffer method invocations, and removeDeadCode() to remove
                 // unused out-values.
                 codeRewriter.rewriteMoveResult(code);
-                DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+                DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
                 consumer.accept(code, method);
                 return null;
               }));
@@ -686,6 +687,7 @@
     codeRewriter.rewriteSwitch(code);
     codeRewriter.processMethodsNeverReturningNormally(code);
     codeRewriter.simplifyIf(code, typeEnvironment);
+    new RedundantFieldLoadElimination(code).run();
 
     if (options.testing.invertConditionals) {
       invertConditionalsForTesting(code);
@@ -705,7 +707,7 @@
     // Dead code removal. Performed after simplifications to remove code that becomes dead
     // as a result of those simplifications. The following optimizations could reveal more
     // dead code which is removed right before register allocation in performRegisterAllocation.
-    DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
     assert code.isConsistentSSA();
 
     if (options.enableDesugaring && enableTryWithResourcesDesugaring()) {
@@ -776,7 +778,7 @@
   private void finalizeToCf(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     assert !method.getCode().isDexCode();
     CfBuilder builder = new CfBuilder(method, code, options.itemFactory);
-    CfCode result = builder.build(codeRewriter, options, appInfo.withSubtyping());
+    CfCode result = builder.build(codeRewriter, graphLense, options, appInfo.withSubtyping());
     method.setCode(result);
     markProcessed(method, code, feedback);
   }
@@ -818,7 +820,7 @@
   private RegisterAllocator performRegisterAllocation(IRCode code, DexEncodedMethod method) {
     // Always perform dead code elimination before register allocation. The register allocator
     // does not allow dead code (to make sure that we do not waste registers for unneeded values).
-    DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+    DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
     materializeInstructionBeforeLongOperationsWorkaround(code, options);
     LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
     registerAllocator.allocateRegisters(options.debug);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index cdb419f..0e4be8b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.stream.Collectors;
@@ -42,10 +43,13 @@
 
   private final GraphLense graphLense;
   private final AppInfoWithSubtyping appInfo;
+  private final InternalOptions options;
 
-  public LensCodeRewriter(GraphLense graphLense, AppInfoWithSubtyping appInfo) {
+  public LensCodeRewriter(
+      GraphLense graphLense, AppInfoWithSubtyping appInfo, InternalOptions options) {
     this.graphLense = graphLense;
     this.appInfo = appInfo;
+    this.options = options;
   }
 
   private Value makeOutValue(Instruction insn, IRCode code) {
@@ -96,8 +100,8 @@
           if (!invokedHolder.isClassType()) {
             continue;
           }
-          DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method);
-          Invoke.Type invokeType = getInvokeType(invoke, actualTarget, invokedMethod);
+          DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method, invoke.getType());
+          Invoke.Type invokeType = getInvokeType(invoke, actualTarget, invokedMethod, method);
           if (actualTarget != invokedMethod || invoke.getType() != invokeType) {
             Invoke newInvoke = Invoke.create(invokeType, actualTarget, null,
                     invoke.outValue(), invoke.inValues());
@@ -107,10 +111,11 @@
                 && newInvoke.outValue() != null) {
               Value newValue = code.createValue(newInvoke.outType(), invoke.getLocalInfo());
               newInvoke.outValue().replaceUsers(newValue);
-              CheckCast cast = new CheckCast(
-                  newValue,
-                  newInvoke.outValue(),
-                  graphLense.lookupType(invokedMethod.proto.returnType, method));
+              CheckCast cast =
+                  new CheckCast(
+                      newValue,
+                      newInvoke.outValue(),
+                      graphLense.lookupType(invokedMethod.proto.returnType));
               cast.setPosition(current.getPosition());
               iterator.add(cast);
               // If the current block has catch handlers split the check cast into its own block.
@@ -123,7 +128,7 @@
         } else if (current.isInstanceGet()) {
           InstanceGet instanceGet = current.asInstanceGet();
           DexField field = instanceGet.getField();
-          DexField actualField = graphLense.lookupField(field, method);
+          DexField actualField = graphLense.lookupField(field);
           if (actualField != field) {
             InstanceGet newInstanceGet =
                 new InstanceGet(
@@ -133,7 +138,7 @@
         } else if (current.isInstancePut()) {
           InstancePut instancePut = current.asInstancePut();
           DexField field = instancePut.getField();
-          DexField actualField = graphLense.lookupField(field, method);
+          DexField actualField = graphLense.lookupField(field);
           if (actualField != field) {
             InstancePut newInstancePut =
                 new InstancePut(
@@ -143,7 +148,7 @@
         } else if (current.isStaticGet()) {
           StaticGet staticGet = current.asStaticGet();
           DexField field = staticGet.getField();
-          DexField actualField = graphLense.lookupField(field, method);
+          DexField actualField = graphLense.lookupField(field);
           if (actualField != field) {
             StaticGet newStaticGet =
                 new StaticGet(staticGet.getType(), staticGet.dest(), actualField);
@@ -152,7 +157,7 @@
         } else if (current.isStaticPut()) {
           StaticPut staticPut = current.asStaticPut();
           DexField field = staticPut.getField();
-          DexField actualField = graphLense.lookupField(field, method);
+          DexField actualField = graphLense.lookupField(field);
           if (actualField != field) {
             StaticPut newStaticPut =
                 new StaticPut(staticPut.getType(), staticPut.inValue(), actualField);
@@ -160,7 +165,7 @@
           }
         } else if (current.isCheckCast()) {
           CheckCast checkCast = current.asCheckCast();
-          DexType newType = graphLense.lookupType(checkCast.getType(), method);
+          DexType newType = graphLense.lookupType(checkCast.getType());
           if (newType != checkCast.getType()) {
             CheckCast newCheckCast =
                 new CheckCast(makeOutValue(checkCast, code), checkCast.object(), newType);
@@ -168,14 +173,14 @@
           }
         } else if (current.isConstClass()) {
           ConstClass constClass = current.asConstClass();
-          DexType newType = graphLense.lookupType(constClass.getValue(), method);
+          DexType newType = graphLense.lookupType(constClass.getValue());
           if (newType != constClass.getValue()) {
             ConstClass newConstClass = new ConstClass(makeOutValue(constClass, code), newType);
             iterator.replaceCurrentInstruction(newConstClass);
           }
         } else if (current.isInstanceOf()) {
           InstanceOf instanceOf = current.asInstanceOf();
-          DexType newType = graphLense.lookupType(instanceOf.type(), method);
+          DexType newType = graphLense.lookupType(instanceOf.type());
           if (newType != instanceOf.type()) {
             InstanceOf newInstanceOf = new InstanceOf(makeOutValue(instanceOf, code),
                 instanceOf.value(), newType);
@@ -183,7 +188,7 @@
           }
         } else if (current.isInvokeNewArray()) {
           InvokeNewArray newArray = current.asInvokeNewArray();
-          DexType newType = graphLense.lookupType(newArray.getArrayType(), method);
+          DexType newType = graphLense.lookupType(newArray.getArrayType());
           if (newType != newArray.getArrayType()) {
             InvokeNewArray newNewArray = new InvokeNewArray(newType, makeOutValue(newArray, code),
                 newArray.inValues());
@@ -191,7 +196,7 @@
           }
         } else if (current.isNewArrayEmpty()) {
           NewArrayEmpty newArrayEmpty = current.asNewArrayEmpty();
-          DexType newType = graphLense.lookupType(newArrayEmpty.type, method);
+          DexType newType = graphLense.lookupType(newArrayEmpty.type);
           if (newType != newArrayEmpty.type) {
             NewArrayEmpty newNewArray = new NewArrayEmpty(makeOutValue(newArrayEmpty, code),
                 newArrayEmpty.size(), newType);
@@ -199,7 +204,7 @@
           }
         } else if (current.isNewInstance()) {
             NewInstance newInstance= current.asNewInstance();
-            DexType newClazz = graphLense.lookupType(newInstance.clazz, method);
+          DexType newClazz = graphLense.lookupType(newInstance.clazz);
             if (newClazz != newInstance.clazz) {
               NewInstance newNewInstance =
                   new NewInstance(newClazz, makeOutValue(newInstance, code));
@@ -215,7 +220,8 @@
       DexEncodedMethod method, DexMethodHandle methodHandle) {
     if (methodHandle.isMethodHandle()) {
       DexMethod invokedMethod = methodHandle.asMethod();
-      DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method);
+      DexMethod actualTarget =
+          graphLense.lookupMethod(invokedMethod, method, methodHandle.type.toInvokeType());
       if (actualTarget != invokedMethod) {
         DexClass clazz = appInfo.definitionFor(actualTarget.holder);
         MethodHandleType newType = methodHandle.type;
@@ -229,7 +235,7 @@
       }
     } else {
       DexField field = methodHandle.asField();
-      DexField actualField = graphLense.lookupField(field, method);
+      DexField actualField = graphLense.lookupField(field);
       if (actualField != field) {
         return new DexMethodHandle(methodHandle.type, actualField);
       }
@@ -240,7 +246,8 @@
   private Type getInvokeType(
       InvokeMethod invoke,
       DexMethod actualTarget,
-      DexMethod originalTarget) {
+      DexMethod originalTarget,
+      DexEncodedMethod invocationContext) {
     // We might move methods from interfaces to classes and vice versa. So we have to support
     // fixing the invoke kind, yet only if it was correct to start with.
     if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
@@ -248,23 +255,42 @@
       DexClass newTargetClass = appInfo.definitionFor(actualTarget.holder);
       if (newTargetClass == null) {
         return invoke.getType();
-      } else {
-        DexClass originalTargetClass = appInfo.definitionFor(originalTarget.holder);
-        if (originalTargetClass != null
-            && (originalTargetClass.isInterface() ^ (invoke.getType() == Type.INTERFACE))) {
-          // The invoke was wrong to start with, so we keep it wrong. This is to ensure we get
-          // the IncompatibleClassChangeError the original invoke would have triggered.
-          return newTargetClass.accessFlags.isInterface()
-              ? Type.VIRTUAL
-              : Type.INTERFACE;
-        } else {
-          return newTargetClass.accessFlags.isInterface()
-              ? Type.INTERFACE
-              : Type.VIRTUAL;
+      }
+      DexClass originalTargetClass = appInfo.definitionFor(originalTarget.holder);
+      if (originalTargetClass != null
+          && (originalTargetClass.isInterface() ^ (invoke.getType() == Type.INTERFACE))) {
+        // The invoke was wrong to start with, so we keep it wrong. This is to ensure we get
+        // the IncompatibleClassChangeError the original invoke would have triggered.
+        return newTargetClass.accessFlags.isInterface() ? Type.VIRTUAL : Type.INTERFACE;
+      }
+      return newTargetClass.accessFlags.isInterface() ? Type.INTERFACE : Type.VIRTUAL;
+    }
+    if (options.enableClassMerging && invoke.isInvokeSuper()) {
+      if (actualTarget.getHolder() == invocationContext.method.getHolder()) {
+        DexClass targetClass = appInfo.definitionFor(actualTarget.holder);
+        if (targetClass == null) {
+          return invoke.getType();
+        }
+
+        // If the super class A of the enclosing class B (i.e., invocationContext.method.holder)
+        // has been merged into B during vertical class merging, and this invoke-super instruction
+        // was resolving to a method in A, then the target method has been changed to a direct
+        // method and moved into B, so that we need to use an invoke-direct instruction instead of
+        // invoke-super.
+        //
+        // At this point, we have an invoke-super instruction where the static target is the
+        // enclosing class. However, such an instruction could occur even if a subclass has never
+        // been merged into the enclosing class. Therefore, to determine if vertical class merging
+        // has been applied, we look if there is a direct method with the right signature, and only
+        // return Type.DIRECT in that case.
+        DexEncodedMethod method = targetClass.lookupDirectMethod(actualTarget);
+        if (method != null) {
+          // The target method has been moved from the super class into the sub class during class
+          // merging such that we now need to use an invoke-direct instruction.
+          return Type.DIRECT;
         }
       }
-    } else {
-      return invoke.getType();
     }
+    return invoke.getType();
   }
 }
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 bd3442c..ae57f48 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
@@ -1707,10 +1707,8 @@
           return values;
         }
       }
-      block =
-          block.exit().isGoto() && !visitedBlocks.contains(block.exit().asGoto().getTarget())
-              ? block.exit().asGoto().getTarget()
-              : null;
+      BasicBlock nextBlock = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+      block = nextBlock != null && !visitedBlocks.contains(nextBlock) ? nextBlock : null;
       it = block != null ? block.listIterator() : null;
     } while (it != null);
     return null;
@@ -1801,7 +1799,9 @@
       // Second pass: remove all the array put instructions for the array for which we have
       // inserted a fill array data instruction instead.
       if (!storesToRemoveForArray.isEmpty()) {
+        Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet();
         do {
+          visitedBlocks.add(block);
           it = block.listIterator();
           while (it.hasNext()) {
             Instruction instruction = it.next();
@@ -1823,7 +1823,8 @@
               }
             }
           }
-          block = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+          BasicBlock nextBlock = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+          block = nextBlock != null && !visitedBlocks.contains(nextBlock) ? nextBlock : null;
         } while (block != null);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index 531989e..8e587b9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.IRCode;
@@ -11,15 +13,19 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.utils.InternalOptions;
+import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Queue;
+import java.util.Set;
 
 public class DeadCodeRemover {
 
   public static void removeDeadCode(
-      IRCode code, CodeRewriter codeRewriter, InternalOptions options) {
-    removeUnneededCatchHandlers(code);
+      IRCode code, CodeRewriter codeRewriter, GraphLense graphLense, InternalOptions options) {
+    removeUnneededCatchHandlers(code, graphLense, options);
     Queue<BasicBlock> worklist = new LinkedList<>();
     worklist.addAll(code.blocks);
     for (BasicBlock block = worklist.poll(); block != null; block = worklist.poll()) {
@@ -98,15 +104,49 @@
     }
   }
 
-  private static void removeUnneededCatchHandlers(IRCode code) {
+  private static void removeUnneededCatchHandlers(
+      IRCode code, GraphLense graphLense, InternalOptions options) {
     for (BasicBlock block : code.blocks) {
-      if (block.hasCatchHandlers() && !block.canThrow()) {
-        CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
-        for (BasicBlock target : handlers.getUniqueTargets()) {
-          target.unlinkCatchHandler();
+      if (block.hasCatchHandlers()) {
+        if (block.canThrow()) {
+          if (options.enableClassMerging) {
+            // Handle the case where an exception class has been merged into its sub class.
+            block.renameGuardsInCatchHandlers(graphLense);
+            unlinkDeadCatchHandlers(block, graphLense);
+          }
+        } else {
+          CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
+          for (BasicBlock target : handlers.getUniqueTargets()) {
+            target.unlinkCatchHandler();
+          }
         }
       }
     }
     code.removeUnreachableBlocks();
   }
+
+  // Due to class merging, it is possible that two exception classes have been merged into one. This
+  // function removes catch handlers where the guards ended up being the same as a previous one.
+  private static void unlinkDeadCatchHandlers(BasicBlock block, GraphLense graphLense) {
+    assert block.hasCatchHandlers();
+    CatchHandlers<BasicBlock> catchHandlers = block.getCatchHandlers();
+    List<DexType> guards = catchHandlers.getGuards();
+    List<BasicBlock> targets = catchHandlers.getAllTargets();
+
+    Set<DexType> previouslySeenGuards = new HashSet<>();
+    List<BasicBlock> deadCatchHandlers = new ArrayList<>();
+    for (int i = 0; i < guards.size(); i++) {
+      // The type may have changed due to class merging.
+      DexType guard = graphLense.lookupType(guards.get(i));
+      boolean guardSeenBefore = !previouslySeenGuards.add(guard);
+      if (guardSeenBefore) {
+        deadCatchHandlers.add(targets.get(i));
+      }
+    }
+    // Remove the guards that are guaranteed to be dead.
+    for (BasicBlock deadCatchHandler : deadCatchHandlers) {
+      deadCatchHandler.unlinkCatchHandler();
+    }
+    assert block.consistentCatchHandlers();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 25cd969..029b169 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -271,7 +271,7 @@
       Origin origin = appInfo.originFor(target.method.holder);
       IRCode code = target.buildInliningIR(appInfo, options, generator, callerPosition, origin);
       if (!target.isProcessed()) {
-        new LensCodeRewriter(graphLense, appInfo).rewrite(code, target);
+        new LensCodeRewriter(graphLense, appInfo, options).rewrite(code, target);
       }
       return code;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
new file mode 100644
index 0000000..24012b1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -0,0 +1,193 @@
+// Copyright (c) 2018, 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;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.FieldInstruction;
+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.Phi;
+import com.android.tools.r8.ir.code.Value;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Eliminate redundant field loads.
+ *
+ * <p>Simple algorithm that goes through all blocks in one pass in dominator order and propagates
+ * active field sets across control-flow edges where the target has only one predecessor.
+ */
+// TODO(ager): Evaluate speed/size for computing active field sets in a fixed-point computation.
+public class RedundantFieldLoadElimination {
+
+  private final IRCode code;
+  private final DominatorTree dominatorTree;
+
+  // Maps keeping track of fields that have an already loaded value at basic block entry.
+  private final HashMap<BasicBlock, HashMap<FieldAndObject, Instruction>>
+      activeInstanceFieldsAtEntry = new HashMap<>();
+  private final HashMap<BasicBlock, HashMap<DexField, Instruction>> activeStaticFieldsAtEntry =
+      new HashMap<>();
+
+  // Maps keeping track of fields with already loaded values for the current block during
+  // elimination.
+  private HashMap<FieldAndObject, Instruction> activeInstanceFields;
+  private HashMap<DexField, Instruction> activeStaticFields;
+
+  public RedundantFieldLoadElimination(IRCode code) {
+    this.code = code;
+    dominatorTree = new DominatorTree(code);
+  }
+
+  private static class FieldAndObject {
+    private final DexField field;
+    private final Value object;
+
+    private FieldAndObject(DexField field, Value receiver) {
+      this.field = field;
+      this.object = receiver;
+    }
+
+    @Override
+    public int hashCode() {
+      return field.hashCode() * 7 + object.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof FieldAndObject)) {
+        return false;
+      }
+      FieldAndObject o = (FieldAndObject) other;
+      return o.object == object && o.field == field;
+    }
+  }
+
+  public void run() {
+    for (BasicBlock block : dominatorTree.getSortedBlocks()) {
+      activeInstanceFields =
+          activeInstanceFieldsAtEntry.containsKey(block)
+              ? activeInstanceFieldsAtEntry.get(block)
+              : new HashMap<>();
+      activeStaticFields =
+          activeStaticFieldsAtEntry.containsKey(block)
+              ? activeStaticFieldsAtEntry.get(block)
+              : new HashMap<>();
+      InstructionListIterator it = block.listIterator();
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+        if (instruction.isFieldInstruction()) {
+          DexField field = instruction.asFieldInstruction().getField();
+          if (instruction.isInstancePut() || instruction.isStaticPut()) {
+            killActiveFields(instruction.asFieldInstruction());
+          } else if (instruction.isInstanceGet() && !instruction.outValue().hasLocalInfo()) {
+            Value object = instruction.asInstanceGet().object();
+            FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+            if (activeInstanceFields.containsKey(fieldAndObject)) {
+              Instruction active = activeInstanceFields.get(fieldAndObject);
+              eliminateRedundantRead(it, instruction, active);
+            } else {
+              activeInstanceFields.put(fieldAndObject, instruction);
+            }
+          } else if (instruction.isStaticGet() && !instruction.outValue().hasLocalInfo()) {
+            if (activeStaticFields.containsKey(field)) {
+              Instruction active = activeStaticFields.get(field);
+              eliminateRedundantRead(it, instruction, active);
+            } else {
+              // A field get on a different class can cause <clinit> to run and change static
+              // field values.
+              killActiveFields(instruction.asFieldInstruction());
+              activeStaticFields.put(field, instruction);
+            }
+          }
+        }
+        if (instruction.isMonitor() || instruction.isInvokeMethod()) {
+          activeInstanceFields.clear();
+          activeStaticFields.clear();
+        }
+      }
+      propagateActiveFieldsFrom(block);
+    }
+    assert code.isConsistentSSA();
+  }
+
+  private void propagateActiveFieldsFrom(BasicBlock block) {
+    for (BasicBlock successor : block.getSuccessors()) {
+      // Allow propagation across exceptional edges, just be careful not to propagate if the
+      // throwing instruction is a field instruction.
+      if (successor.getPredecessors().size() == 1) {
+        if (block.hasCatchSuccessor(successor)) {
+          Instruction exceptionalExit = block.exceptionalExit();
+          if (exceptionalExit != null && exceptionalExit.isFieldInstruction()) {
+            killActiveFieldsForExceptionalExit(exceptionalExit.asFieldInstruction());
+          }
+        }
+        assert !activeInstanceFieldsAtEntry.containsKey(successor);
+        activeInstanceFieldsAtEntry.put(successor, new HashMap<>(activeInstanceFields));
+        assert !activeStaticFieldsAtEntry.containsKey(successor);
+        activeStaticFieldsAtEntry.put(successor, new HashMap<>(activeStaticFields));
+      }
+    }
+  }
+
+  private void killActiveFields(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstancePut()) {
+      // Remove all the field/object pairs that refer to this field to make sure
+      // that we are conservative.
+      List<FieldAndObject> keysToRemove = new ArrayList<>();
+      for (FieldAndObject key : activeInstanceFields.keySet()) {
+        if (key.field == field) {
+          keysToRemove.add(key);
+        }
+      }
+      keysToRemove.forEach((k) -> activeInstanceFields.remove(k));
+    } else if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticPut()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      } else {
+        activeStaticFields.remove(field);
+      }
+    } else if (instruction.isStaticGet()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      }
+    }
+  }
+
+  // If a field get instruction throws an exception it did not have an effect on the
+  // value of the field. Therefore, when propagating across exceptional edges for a
+  // field get instruction we have to exclude that field from the set of known
+  // field values.
+  private void killActiveFieldsForExceptionalExit(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticGet()) {
+      activeStaticFields.remove(field);
+    }
+  }
+
+  private void eliminateRedundantRead(
+      InstructionListIterator it, Instruction redundant, Instruction active) {
+    redundant.outValue().replaceUsers(active.outValue());
+    it.removeOrReplaceByDebugLocalRead();
+    active.outValue().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index d2c7701..a9df6a3 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -195,7 +195,8 @@
       transformBridgeMethod();
     }
     computeNeedsRegister();
-    insertArgumentMoves();
+    constrainArgumentIntervals();
+    insertRangeInvokeMoves();
     ImmutableList<BasicBlock> blocks = computeLivenessInformation();
     // First attempt to allocate register allowing argument reuse. This will fail if spilling
     // is required or if we end up using more than 16 registers.
@@ -2598,11 +2599,14 @@
     }
   }
 
-  private void insertArgumentMoves() {
+  private void constrainArgumentIntervals() {
     // Record the constraint that incoming arguments are in consecutive registers.
     List<Value> arguments = code.collectArguments();
     createArgumentLiveIntervals(arguments);
     linkArgumentValuesAndIntervals(arguments);
+  }
+
+  private void insertRangeInvokeMoves() {
     for (BasicBlock block : code.blocks) {
       InstructionListIterator it = block.listIterator();
       while (it.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
index 0ba49d3..6ea7d60 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
@@ -45,6 +45,7 @@
     assert checkSignatures();
 
     switch (invokeType) {
+      case DIRECT:
       case STATIC:
       case SUPER:
       case INTERFACE:
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 56e381b..01fff01 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -8,9 +8,11 @@
 import static com.android.tools.r8.utils.DescriptorUtils.getPackageBinaryNameFromJavaType;
 
 import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
@@ -18,16 +20,20 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.signature.GenericSignatureAction;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import java.lang.reflect.GenericSignatureFormatError;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -40,6 +46,7 @@
 
   private final AppInfoWithLiveness appInfo;
   private final RootSet rootSet;
+  private final Reporter reporter;
   private final PackageObfuscationMode packageObfuscationMode;
   private final boolean isAccessModificationAllowed;
   private final Set<String> noObfuscationPrefixes = Sets.newHashSet();
@@ -66,6 +73,7 @@
       InternalOptions options) {
     this.appInfo = appInfo;
     this.rootSet = rootSet;
+    this.reporter = options.reporter;
     this.packageObfuscationMode = options.proguardConfiguration.getPackageObfuscationMode();
     this.isAccessModificationAllowed = options.proguardConfiguration.isAccessModificationAllowed();
     this.packageDictionary = options.proguardConfiguration.getPackageObfuscationDictionary();
@@ -152,27 +160,77 @@
     }
   }
 
+  private void parseError(DexItem item, Origin origin, GenericSignatureFormatError e) {
+    StringBuilder message = new StringBuilder("Invalid signature for ");
+    if (item instanceof DexClass) {
+      message.append("class ");
+      message.append(((DexClass) item).getType().toSourceString());
+    } else if (item instanceof DexEncodedField) {
+      message.append("field ");
+      message.append(item.toSourceString());
+    } else {
+      assert item instanceof DexEncodedMethod;
+      message.append("method ");
+      message.append(item.toSourceString());
+    }
+    message.append(".\n");
+    message.append(e.getMessage());
+    reporter.warning(new StringDiagnostic(message.toString(), origin));
+  }
+
   private void renameTypesInGenericSignatures() {
     for (DexClass clazz : appInfo.classes()) {
-      rewriteGenericSignatures(clazz.annotations.annotations,
-          genericSignatureParser::parseClassSignature);
-      clazz.forEachField(field -> rewriteGenericSignatures(
-          field.annotations.annotations, genericSignatureParser::parseFieldSignature));
-      clazz.forEachMethod(method -> rewriteGenericSignatures(
-          method.annotations.annotations, genericSignatureParser::parseMethodSignature));
+      clazz.annotations = rewriteGenericSignatures(clazz.annotations,
+          genericSignatureParser::parseClassSignature,
+          e -> parseError(clazz, clazz.getOrigin(), e));
+      clazz.forEachField(field ->
+          field.annotations = rewriteGenericSignatures(
+              field.annotations, genericSignatureParser::parseFieldSignature,
+              e -> parseError(field, clazz.getOrigin(), e)));
+      clazz.forEachMethod(method ->
+        method.annotations = rewriteGenericSignatures(
+            method.annotations, genericSignatureParser::parseMethodSignature,
+            e -> parseError(method, clazz.getOrigin(), e)));
     }
   }
 
-  private void rewriteGenericSignatures(DexAnnotation[] annotations, Consumer<String> parser) {
-    for (int i = 0; i < annotations.length; i++) {
-      DexAnnotation annotation = annotations[i];
+  private DexAnnotationSet rewriteGenericSignatures(
+      DexAnnotationSet annotations,
+      Consumer<String> parser,
+      Consumer<GenericSignatureFormatError> parseError) {
+    // There can be no more than one signature annotation in an annotation set.
+    final int VALID = -1;
+    int invalid = VALID;
+    for (int i = 0; i < annotations.annotations.length && invalid == VALID; i++) {
+      DexAnnotation annotation = annotations.annotations[i];
       if (DexAnnotation.isSignatureAnnotation(annotation, appInfo.dexItemFactory)) {
-        parser.accept(DexAnnotation.getSignature(annotation));
-        annotations[i] = DexAnnotation.createSignatureAnnotation(
-            genericSignatureRewriter.getRenamedSignature(),
-            appInfo.dexItemFactory);
+        try {
+          parser.accept(DexAnnotation.getSignature(annotation));
+          annotations.annotations[i] = DexAnnotation.createSignatureAnnotation(
+              genericSignatureRewriter.getRenamedSignature(),
+              appInfo.dexItemFactory);
+        } catch (GenericSignatureFormatError e) {
+          parseError.accept(e);
+          invalid = i;
+        }
       }
     }
+
+    // Return the rewritten signatures if it was valid and could be rewritten.
+    if (invalid == VALID) {
+      return annotations;
+    }
+    // Remove invalid signature if found.
+    DexAnnotation[] prunedAnnotations =
+        new DexAnnotation[annotations.annotations.length - 1];
+    int dest = 0;
+    for (int i = 0; i < annotations.annotations.length; i++) {
+      if (i != invalid) {
+        prunedAnnotations[dest++] = annotations.annotations[i];
+      }
+    }
+    assert dest == prunedAnnotations.length;
+    return new DexAnnotationSet(prunedAnnotations);
   }
 
   /**
@@ -450,12 +508,18 @@
       String enclosingRenamedBinaryName =
           getClassBinaryNameFromDescriptor(
               renaming.getOrDefault(enclosingType, enclosingType.descriptor).toString());
-      String renamed =
-          getClassBinaryNameFromDescriptor(
-              renaming.getOrDefault(type, type.descriptor).toString());
-      assert renamed.startsWith(enclosingRenamedBinaryName + Minifier.INNER_CLASS_SEPARATOR);
-      String outName = renamed.substring(enclosingRenamedBinaryName.length() + 1);
-      renamedSignature.append(outName);
+      DexString renamedDescriptor = renaming.get(type);
+      if (renamedDescriptor != null) {
+        // Pick the renamed inner class from the fully renamed binary name.
+        String fullRenamedBinaryName =
+            getClassBinaryNameFromDescriptor(renamedDescriptor.toString());
+        renamedSignature.append(
+            fullRenamedBinaryName.substring(enclosingRenamedBinaryName.length() + 1));
+      } else {
+        // Did not find the class - keep the inner class name as is.
+        // TODO(110085899): Warn about missing classes in signatures?
+        renamedSignature.append(name);
+      }
       return type;
     }
 
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
index 521229e..7dfd840 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
@@ -37,6 +37,7 @@
       AppInfoWithLiveness appInfo,
       GraphLense previousLense,
       SeedMapper seedMapper) {
+    assert previousLense.isContextFreeForMethods();
     this.appInfo = appInfo;
     this.previousLense = previousLense;
     this.seedMapper = seedMapper;
@@ -44,7 +45,7 @@
 
   public GraphLense run(Timing timing) {
     timing.begin("from-pg-map-to-lense");
-    GraphLense lenseFromMap = new MapToLenseConverter().run(previousLense);
+    GraphLense lenseFromMap = new MapToLenseConverter().run();
     timing.end();
     timing.begin("fix-types-in-programs");
     GraphLense typeFixedLense = new TreeFixer(lenseFromMap).run();
@@ -61,7 +62,7 @@
       lenseBuilder = new ConflictFreeBuilder();
     }
 
-    private GraphLense run(GraphLense previousLense) {
+    private GraphLense run() {
       // To handle inherited yet undefined methods in library classes, we are traversing types in
       // a subtyping order. That also helps us detect conflicted mappings in a diamond case:
       //   LibItfA#foo -> a, LibItfB#foo -> b, while PrgA implements LibItfA and LibItfB.
@@ -308,7 +309,7 @@
       }
       for (int i = 0; i < methods.length; i++) {
         DexEncodedMethod encodedMethod = methods[i];
-        DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method, encodedMethod);
+        DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method);
         DexType newHolderType = substituteType(appliedMethod.holder, encodedMethod);
         DexProto newProto = substituteTypesIn(appliedMethod.proto, encodedMethod);
         DexMethod newMethod;
@@ -331,7 +332,7 @@
       }
       for (int i = 0; i < fields.length; i++) {
         DexEncodedField encodedField = fields[i];
-        DexField appliedField = appliedLense.lookupField(encodedField.field, null);
+        DexField appliedField = appliedLense.lookupField(encodedField.field);
         DexType newHolderType = substituteType(appliedField.clazz, null);
         DexType newFieldType = substituteType(appliedField.type, null);
         DexField newField;
@@ -427,7 +428,7 @@
           return type.replaceBaseType(fixed, appInfo.dexItemFactory);
         }
       }
-      return appliedLense.lookupType(type, context);
+      return appliedLense.lookupType(type);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
index 0cc0d53..c1f6a7d 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
@@ -75,24 +75,51 @@
   }
 
   public void parseClassSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseClassSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseClassSignature();
+      actions.stop();
+    } catch (GenericSignatureFormatError e) {
+      throw e;
+    } catch (Throwable t) {
+      Error e = new GenericSignatureFormatError(
+          "Unknown error parsing class signature: " + t.getMessage());
+      e.addSuppressed(t);
+      throw e;
+    }
   }
 
   public void parseMethodSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseMethodTypeSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseMethodTypeSignature();
+      actions.stop();
+    } catch (GenericSignatureFormatError e) {
+      throw e;
+    } catch (Throwable t) {
+      Error e = new GenericSignatureFormatError(
+          "Unknown error parsing method signature: " + t.getMessage());
+      e.addSuppressed(t);
+      throw e;
+    }
   }
 
   public void parseFieldSignature(String signature) {
-    actions.start();
-    setInput(signature);
-    parseFieldTypeSignature();
-    actions.stop();
+    try {
+      actions.start();
+      setInput(signature);
+      parseFieldTypeSignature();
+      actions.stop();
+    } catch (GenericSignatureFormatError e) {
+      throw e;
+    } catch (Throwable t) {
+      Error e = new GenericSignatureFormatError(
+          "Unknown error parsing field signature: " + t.getMessage());
+      e.addSuppressed(t);
+      throw e;
+    }
   }
 
   private void setInput(String input) {
@@ -178,7 +205,7 @@
         updateTypeVariableSignature();
         break;
       default:
-        parseError();
+        parseError("Expected L, [ or T", pos);
     }
   }
 
@@ -341,15 +368,18 @@
         eof = true;
       }
     } else {
-      parseError("Unexpected end of signature");
+      parseError("Unexpected end of signature", pos);
     }
   }
 
   private void expect(char c) {
+    if (eof) {
+      parseError("Unexpected end of signature", pos);
+    }
     if (symbol == c) {
       scanSymbol();
     } else {
-      parseError("Expected " + c);
+      parseError("Expected " + c, pos - 1);
     }
   }
 
@@ -369,7 +399,7 @@
   // PRE: symbol is the first char of the identifier.
   // POST: symbol = the next symbol AFTER the identifier.
   private void scanIdentifier() {
-    if (!eof) {
+    if (!eof && pos < buffer.length) {
       StringBuilder identBuf = new StringBuilder(32);
       if (!isStopSymbol(symbol)) {
         identBuf.append(symbol);
@@ -399,15 +429,15 @@
         parseError();
       }
     } else {
-      parseError("Unexpected end of signature");
+      parseError("Unexpected end of signature", pos);
     }
   }
 
   private void parseError() {
-    parseError("Unexpected");
+    parseError("Unexpected", pos);
   }
 
-  private void parseError(String message) {
+  private void parseError(String message, int pos) {
     String arrow = CharBuffer.allocate(pos).toString().replace('\0', ' ') + '^';
     throw new GenericSignatureFormatError(
         message + " at position " + (pos + 1) + "\n" + String.valueOf(buffer) + "\n" + arrow);
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
index c68122c..56089f4 100644
--- a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
@@ -43,16 +44,17 @@
       InvokeKind kind = targetExtractor.getKind();
       if (target != null && target.getArity() == method.method.getArity()) {
         assert !method.accessFlags.isPrivate() && !method.accessFlags.isConstructor();
-        target = lense.lookupMethod(target, method);
         if (kind == InvokeKind.STATIC) {
           assert method.accessFlags.isStatic();
-          DexEncodedMethod targetMethod = appInfo.lookupStaticTarget(target);
+          DexMethod actualTarget = lense.lookupMethod(target, method, Type.STATIC);
+          DexEncodedMethod targetMethod = appInfo.lookupStaticTarget(actualTarget);
           if (targetMethod != null) {
             addForwarding(method, targetMethod);
           }
         } else if (kind == InvokeKind.VIRTUAL) {
           // TODO(herhut): Add support for bridges with multiple targets.
-          DexEncodedMethod targetMethod = appInfo.lookupSingleVirtualTarget(target);
+          DexMethod actualTarget = lense.lookupMethod(target, method, Type.VIRTUAL);
+          DexEncodedMethod targetMethod = appInfo.lookupSingleVirtualTarget(actualTarget);
           if (targetMethod != null) {
             addForwarding(method, targetMethod);
           }
@@ -86,31 +88,29 @@
     }
 
     @Override
-    public DexType lookupType(DexType type, DexEncodedMethod context) {
-      return previousLense.lookupType(type, context);
+    public DexType lookupType(DexType type) {
+      return previousLense.lookupType(type);
     }
 
     @Override
-    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
-      DexMethod previous = previousLense.lookupMethod(method, context);
+    public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+      DexMethod previous = previousLense.lookupMethod(method, context, type);
       DexMethod bridge = bridgeTargetToBridgeMap.get(previous);
       // Do not forward calls from a bridge method to itself while the bridge method is still
       // a bridge.
-      if (bridge == null
-          || (context.accessFlags.isBridge() && bridge == context.method)) {
+      if (bridge == null || (context.accessFlags.isBridge() && bridge == context.method)) {
         return previous;
-      } else {
-        return bridge;
       }
+      return bridge;
     }
 
     @Override
-    public DexField lookupField(DexField field, DexEncodedMethod context) {
-      return previousLense.lookupField(field, context);
+    public DexField lookupField(DexField field) {
+      return previousLense.lookupField(field);
     }
 
     @Override
-    public boolean isContextFree() {
+    public boolean isContextFreeForMethods() {
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
similarity index 87%
rename from src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java
rename to src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
index 7a13348..297dd57 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
@@ -1,7 +1,13 @@
 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.graph;
+package com.android.tools.r8.optimize;
+
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.MethodAccessFlags;
 
 public final class ClassAndMemberPublicizer {
   private final DexItemFactory factory;
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 488cf2f..fc408e3 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -29,7 +29,7 @@
   private final GraphLense.Builder builder = GraphLense.builder();
 
   public MemberRebindingAnalysis(AppInfoWithLiveness appInfo, GraphLense lense) {
-    assert lense.isContextFree();
+    assert lense.isContextFreeForMethods();
     this.appInfo = appInfo;
     this.lense = lense;
   }
@@ -115,7 +115,7 @@
   private void computeMethodRebinding(Set<DexMethod> methods,
       Function<DexMethod, DexEncodedMethod> lookupTarget) {
     for (DexMethod method : methods) {
-      method = lense.lookupMethod(method, null);
+      method = lense.lookupMethod(method);
       // We can safely ignore array types, as the corresponding methods are defined in a library.
       if (!method.getHolder().isClassType()) {
         continue;
@@ -188,7 +188,7 @@
       BiFunction<DexClass, DexField, DexEncodedField> lookupTargetOnClass) {
     for (Map.Entry<DexField, Set<DexEncodedMethod>> entry : fields.entrySet()) {
       DexField field = entry.getKey();
-      field = lense.lookupField(field, null);
+      field = lense.lookupField(field);
       DexEncodedField target = lookup.apply(field.getHolder(), field);
       // Rebind to the lowest library class or program class. Do not rebind accesses to fields that
       // are not visible from the access context.
diff --git a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
index 5847a80..ebf110e 100644
--- a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
 import com.google.common.collect.Sets;
@@ -28,7 +29,8 @@
   }
 
   private void identifyBridgeMethod(DexEncodedMethod method) {
-    if (method.accessFlags.isBridge()) {
+    MethodAccessFlags accessFlags = method.accessFlags;
+    if (accessFlags.isBridge() && !accessFlags.isAbstract()) {
       InvokeSingleTargetExtractor targetExtractor = new InvokeSingleTargetExtractor();
       method.getCode().registerCodeReferences(targetExtractor);
       DexMethod target = targetExtractor.getTarget();
@@ -36,11 +38,11 @@
       // javac-generated visibility forward bridge method has same descriptor (name, signature and
       // return type).
       if (target != null && target.hasSameProtoAndName(method.method)) {
-        assert !method.accessFlags.isPrivate() && !method.accessFlags.isConstructor();
+        assert !accessFlags.isPrivate() && !accessFlags.isConstructor();
         if (kind == InvokeKind.SUPER) {
           // This is a visibility forward, so check for the direct target.
-          DexEncodedMethod targetMethod
-              = appInfo.resolveMethod(target.getHolder(), target).asSingleTarget();
+          DexEncodedMethod targetMethod =
+              appInfo.resolveMethod(target.getHolder(), target).asSingleTarget();
           if (targetMethod != null && targetMethod.accessFlags.isPublic()) {
             if (Log.ENABLED) {
               Log.info(getClass(), "Removing visibility forwarding %s -> %s", method.method,
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 085d9b3..2b51720 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -71,7 +71,6 @@
 import java.util.SortedSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -734,6 +733,7 @@
   private void transitionDefaultMethodsForInstantiatedClass(DexType iface, DexType instantiatedType,
       ScopedDexMethodSet seen) {
     DexClass clazz = appInfo.definitionFor(iface);
+    assert clazz != null : "Missing class " + iface.toSourceString();
     assert clazz.accessFlags.isInterface();
     SetWithReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(iface);
     if (reachableMethods != null) {
@@ -1638,8 +1638,8 @@
       this.liveTypes = rewriteItems(previous.liveTypes, lense::lookupType);
       this.instantiatedTypes = rewriteItems(previous.instantiatedTypes, lense::lookupType);
       this.instantiatedLambdas = rewriteItems(previous.instantiatedLambdas, lense::lookupType);
-      this.targetedMethods = rewriteItems(previous.targetedMethods, lense::lookupMethod);
-      this.liveMethods = rewriteItems(previous.liveMethods, lense::lookupMethod);
+      this.targetedMethods = rewriteMethodsConservatively(previous.targetedMethods, lense);
+      this.liveMethods = rewriteMethodsConservatively(previous.liveMethods, lense);
       this.liveFields = rewriteItems(previous.liveFields, lense::lookupField);
       this.instanceFieldReads =
           rewriteKeysWhileMergingValues(previous.instanceFieldReads, lense::lookupField);
@@ -1652,11 +1652,11 @@
       this.fieldsRead = rewriteItems(previous.fieldsRead, lense::lookupField);
       this.fieldsWritten = rewriteItems(previous.fieldsWritten, lense::lookupField);
       this.pinnedItems = rewriteMixedItems(previous.pinnedItems, lense);
-      this.virtualInvokes = rewriteItems(previous.virtualInvokes, lense::lookupMethod);
-      this.interfaceInvokes = rewriteItems(previous.interfaceInvokes, lense::lookupMethod);
-      this.superInvokes = rewriteItems(previous.superInvokes, lense::lookupMethod);
-      this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
-      this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
+      this.virtualInvokes = rewriteMethodsConservatively(previous.virtualInvokes, lense);
+      this.interfaceInvokes = rewriteMethodsConservatively(previous.interfaceInvokes, lense);
+      this.superInvokes = rewriteMethodsConservatively(previous.superInvokes, lense);
+      this.directInvokes = rewriteMethodsConservatively(previous.directInvokes, lense);
+      this.staticInvokes = rewriteMethodsConservatively(previous.staticInvokes, lense);
       this.prunedTypes = rewriteItems(previous.prunedTypes, lense::lookupType);
       // TODO(herhut): Migrate these to Descriptors, as well.
       assert assertNotModifiedByLense(previous.noSideEffects.keySet(), lense);
@@ -1738,16 +1738,16 @@
       for (DexItem item : items) {
         if (item instanceof DexClass) {
           DexType type = ((DexClass) item).type;
-          assert lense.lookupType(type, null) == type;
+          assert lense.lookupType(type) == type;
         } else if (item instanceof DexEncodedMethod) {
           DexEncodedMethod method = (DexEncodedMethod) item;
           // We only allow changes to bridge methods, as these get retargeted even if they
           // are kept.
           assert method.accessFlags.isBridge()
-              || lense.lookupMethod(method.method, null) == method.method;
+              || lense.lookupMethod(method.method) == method.method;
         } else if (item instanceof DexEncodedField) {
           DexField field = ((DexEncodedField) item).field;
-          assert lense.lookupField(field, null) == field;
+          assert lense.lookupField(field) == field;
         } else {
           assert false;
         }
@@ -1792,31 +1792,54 @@
       return builder.build();
     }
 
+    private static ImmutableSortedSet<DexMethod> rewriteMethodsConservatively(
+        Set<DexMethod> original, GraphLense lense) {
+      ImmutableSortedSet.Builder<DexMethod> builder =
+          new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
+      if (lense.isContextFreeForMethods()) {
+        for (DexMethod item : original) {
+          builder.add(lense.lookupMethod(item));
+        }
+      } else {
+        for (DexMethod item : original) {
+          // Avoid using lookupMethodInAllContexts when possible.
+          if (lense.isContextFreeForMethod(item)) {
+            builder.add(lense.lookupMethod(item));
+          } else {
+            // The lense is context sensitive, but we do not have the context here. Therefore, we
+            // conservatively look up the method in all contexts.
+            builder.addAll(lense.lookupMethodInAllContexts(item));
+          }
+        }
+      }
+      return builder.build();
+    }
+
     private static <T extends PresortedComparable<T>> ImmutableSortedSet<T> rewriteItems(
-        Set<T> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+        Set<T> original, Function<T, T> rewrite) {
       ImmutableSortedSet.Builder<T> builder =
           new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
       for (T item : original) {
-        builder.add(rewrite.apply(item, null));
+        builder.add(rewrite.apply(item));
       }
       return builder.build();
     }
 
     private static <T extends PresortedComparable<T>, S> ImmutableMap<T, S> rewriteKeys(
-        Map<T, S> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+        Map<T, S> original, Function<T, T> rewrite) {
       ImmutableMap.Builder<T, S> builder = new ImmutableMap.Builder<>();
       for (T item : original.keySet()) {
-        builder.put(rewrite.apply(item, null), original.get(item));
+        builder.put(rewrite.apply(item), original.get(item));
       }
       return builder.build();
     }
 
-    private static <T extends PresortedComparable<T>, S> Map<T, Set<S>>
-        rewriteKeysWhileMergingValues(
-            Map<T, Set<S>> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+    private static <T extends PresortedComparable<T>, S>
+        Map<T, Set<S>> rewriteKeysWhileMergingValues(
+            Map<T, Set<S>> original, Function<T, T> rewrite) {
       Map<T, Set<S>> result = new IdentityHashMap<>();
       for (T item : original.keySet()) {
-        T rewrittenKey = rewrite.apply(item, null);
+        T rewrittenKey = rewrite.apply(item);
         result.computeIfAbsent(rewrittenKey, k -> Sets.newIdentityHashSet())
             .addAll(original.get(item));
       }
@@ -1829,11 +1852,11 @@
       for (DexItem item : original) {
         // TODO(b/67934123) There should be a common interface to perform the dispatch.
         if (item instanceof DexType) {
-          builder.add(lense.lookupType((DexType) item, null));
+          builder.add(lense.lookupType((DexType) item));
         } else if (item instanceof DexMethod) {
-          builder.add(lense.lookupMethod((DexMethod) item, null));
+          builder.add(lense.lookupMethod((DexMethod) item));
         } else if (item instanceof DexField) {
-          builder.add(lense.lookupField((DexField) item, null));
+          builder.add(lense.lookupField((DexField) item));
         } else {
           throw new Unreachable();
         }
@@ -1892,7 +1915,6 @@
 
     public AppInfoWithLiveness rewrittenWithLense(DirectMappedDexApplication application,
         GraphLense lense) {
-      assert lense.isContextFree();
       return new AppInfoWithLiveness(this, application, lense);
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java b/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
index 1eb11e9..c67af00 100644
--- a/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
+++ b/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
@@ -46,14 +46,14 @@
     return path;
   }
 
-  public boolean matchesFile(Path file) {
+  public boolean matchesFile(String name) {
     if (isUnfiltered()) {
       return true;
     }
     boolean isNegated = false;
     for (String pattern : pattern) {
       isNegated = pattern.charAt(0) == '!';
-      boolean matches = matchAgainstFileName(file.toString(), 0, pattern, isNegated ? 1 : 0);
+      boolean matches = matchAgainstFileName(name, 0, pattern, isNegated ? 1 : 0);
       if (matches) {
         return !isNegated;
       }
@@ -63,7 +63,7 @@
   }
 
   private boolean containsFileSeparator(String string) {
-    return string.indexOf(File.separatorChar) != -1;
+    return string.indexOf('/') != -1;
   }
 
   private boolean matchAgainstFileName(String fileName, int namePos, String pattern,
@@ -95,7 +95,7 @@
         }
       } else {
         for (int i = namePos; i < fileName.length(); i++) {
-          if (!includeFileSeparators && fileName.charAt(i) == File.separatorChar) {
+          if (!includeFileSeparators && fileName.charAt(i) == '/') {
             return false;
           }
           if (matchAgainstFileName(fileName, i, pattern, patternPos + 1)) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index ed9cbdb..c4276dc 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -812,45 +812,45 @@
         skipWhitespace();
         switch (peekChar()) {
           case 'a':
-            if (found = acceptString("abstract")) {
+            if ((found = acceptString("abstract"))) {
               flags.setAbstract();
             }
             break;
           case 'f':
-            if (found = acceptString("final")) {
+            if ((found = acceptString("final"))) {
               flags.setFinal();
             }
             break;
           case 'n':
-            if (found = acceptString("native")) {
+            if ((found = acceptString("native"))) {
               flags.setNative();
             }
             break;
           case 'p':
-            if (found = acceptString("public")) {
+            if ((found = acceptString("public"))) {
               flags.setPublic();
-            } else if (found = acceptString("private")) {
+            } else if ((found = acceptString("private"))) {
               flags.setPrivate();
-            } else if (found = acceptString("protected")) {
+            } else if ((found = acceptString("protected"))) {
               flags.setProtected();
             }
             break;
           case 's':
-            if (found = acceptString("synchronized")) {
+            if ((found = acceptString("synchronized"))) {
               flags.setSynchronized();
-            } else if (found = acceptString("static")) {
+            } else if ((found = acceptString("static"))) {
               flags.setStatic();
-            } else if (found = acceptString("strictfp")) {
+            } else if ((found = acceptString("strictfp"))) {
               flags.setStrict();
             }
             break;
           case 't':
-            if (found = acceptString("transient")) {
+            if ((found = acceptString("transient"))) {
               flags.setTransient();
             }
             break;
           case 'v':
-            if (found = acceptString("volatile")) {
+            if ((found = acceptString("volatile"))) {
               flags.setVolatile();
             }
             break;
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 700e146..c860d83 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -43,6 +43,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 public class RootSetBuilder {
@@ -441,17 +442,28 @@
   }
 
   // TODO(67934426): Test this code.
-  public static void writeSeeds(AppInfoWithLiveness appInfo, PrintStream out) {
+  public static void writeSeeds(
+      AppInfoWithLiveness appInfo, PrintStream out, Predicate<DexType> include) {
     for (DexItem seed : appInfo.getPinnedItems()) {
       if (seed instanceof DexType) {
-        out.println(seed.toSourceString());
+        if (include.test((DexType) seed)) {
+          out.println(seed.toSourceString());
+        }
       } else if (seed instanceof DexField) {
         DexField field = ((DexField) seed);
-        out.println(
-            field.clazz.toSourceString() + ": " + field.type.toSourceString() + " " + field.name
-                .toSourceString());
+        if (include.test(field.clazz)) {
+          out.println(
+              field.clazz.toSourceString()
+                  + ": "
+                  + field.type.toSourceString()
+                  + " "
+                  + field.name.toSourceString());
+        }
       } else if (seed instanceof DexMethod) {
         DexMethod method = (DexMethod) seed;
+        if (!include.test(method.holder)) {
+          continue;
+        }
         out.print(method.holder.toSourceString() + ": ");
         DexEncodedMethod encodedMethod = appInfo.definitionFor(method);
         if (encodedMethod.accessFlags.isConstructor()) {
diff --git a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
deleted file mode 100644
index a80e758..0000000
--- a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
+++ /dev/null
@@ -1,667 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.shaking;
-
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
-import com.android.tools.r8.graph.DexString;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.graph.GraphLense;
-import com.android.tools.r8.graph.GraphLense.Builder;
-import com.android.tools.r8.graph.KeyedDexItem;
-import com.android.tools.r8.graph.PresortedComparable;
-import com.android.tools.r8.logging.Log;
-import com.android.tools.r8.optimize.InvokeSingleTargetExtractor;
-import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.Timing;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterators;
-import it.unimi.dsi.fastutil.ints.Int2IntMap;
-import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.BiFunction;
-import java.util.stream.Collectors;
-
-/**
- * Merges Supertypes with a single implementation into their single subtype.
- * <p>
- * A common use-case for this is to merge an interface into its single implementation.
- * <p>
- * The class merger only fixes the structure of the graph but leaves the actual instructions
- * untouched. Fixup of instructions is deferred via a {@link GraphLense} to the Ir building phase.
- */
-public class SimpleClassMerger {
-
-  private final DexApplication application;
-  private final AppInfoWithLiveness appInfo;
-  private final GraphLense graphLense;
-  private final GraphLense.Builder renamedMembersLense = GraphLense.builder();
-  private final Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
-  private final Timing timing;
-  private Collection<DexMethod> invokes;
-  private int numberOfMerges = 0;
-
-  public SimpleClassMerger(DexApplication application, AppInfoWithLiveness appInfo,
-      GraphLense graphLense, Timing timing) {
-    this.application = application;
-    this.appInfo = appInfo;
-    this.graphLense = graphLense;
-    this.timing = timing;
-  }
-
-  private boolean isMergeCandidate(DexProgramClass clazz) {
-    // We can merge program classes if they are not instantiated, have a single subtype
-    // and we do not have to keep them.
-    return !clazz.isLibraryClass()
-        && !appInfo.instantiatedTypes.contains(clazz.type)
-        && !appInfo.isPinned(clazz.type)
-        && clazz.type.getSingleSubtype() != null;
-  }
-
-  private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
-      Equivalence<DexMethod> equivalence) {
-    DexClass definition = appInfo.definitionFor(method.holder);
-    if (definition != null && definition.isProgramClass()) {
-      set.add(equivalence.wrap(method));
-    }
-  }
-
-  private Collection<DexMethod> getInvokes() {
-    if (invokes == null) {
-      // Collect all reachable methods that are not within a library class. Those defined on
-      // library classes are known not to have program classes in their signature.
-      // Also filter methods that only use types from library classes in their signatures. We
-      // know that those won't conflict.
-      Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
-      Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
-      appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
-      invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
-          .collect(Collectors.toList());
-    }
-    return invokes;
-  }
-
-  private boolean isProgramClass(DexType type) {
-    if (type.isArrayType()) {
-      type = type.toBaseType(appInfo.dexItemFactory);
-    }
-    if (type.isClassType()) {
-      DexClass clazz = appInfo.definitionFor(type);
-      if (clazz != null && clazz.isProgramClass()) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private boolean removeNonProgram(DexMethod dexMethod) {
-    for (DexType type : dexMethod.proto.parameters.values) {
-      if (isProgramClass(type)) {
-        return true;
-      }
-    }
-    return isProgramClass(dexMethod.proto.returnType);
-  }
-
-  public GraphLense run() {
-    timing.begin("merge");
-    GraphLense mergingGraphLense = mergeClasses(graphLense);
-    timing.end();
-    timing.begin("fixup");
-    GraphLense result = new TreeFixer().fixupTypeReferences(mergingGraphLense);
-    timing.end();
-    return result;
-  }
-
-  private GraphLense mergeClasses(GraphLense graphLense) {
-    for (DexProgramClass clazz : application.classes()) {
-      if (isMergeCandidate(clazz)) {
-        DexClass targetClass = appInfo.definitionFor(clazz.type.getSingleSubtype());
-        if (appInfo.isPinned(targetClass.type)) {
-          // We have to keep the target class intact, so we cannot merge it.
-          continue;
-        }
-        if (mergedClasses.containsKey(targetClass.type)) {
-          // TODO(herhut): Traverse top-down.
-          continue;
-        }
-        if (clazz.hasClassInitializer() && targetClass.hasClassInitializer()) {
-          // TODO(herhut): Handle class initializers.
-          if (Log.ENABLED) {
-            Log.info(getClass(), "Cannot merge %s into %s due to static initializers.",
-                clazz.toSourceString(), targetClass.toSourceString());
-          }
-          continue;
-        }
-        // Guard against the case where we have two methods that may get the same signature
-        // if we replace types. This is rare, so we approximate and err on the safe side here.
-        if (new CollisionDetector(clazz.type, targetClass.type, getInvokes(), mergedClasses)
-            .mayCollide()) {
-          if (Log.ENABLED) {
-            Log.info(getClass(), "Cannot merge %s into %s due to conflict.", clazz.toSourceString(),
-                targetClass.toSourceString());
-          }
-          continue;
-        }
-        boolean merged = new ClassMerger(clazz, targetClass).merge();
-        if (Log.ENABLED) {
-          if (merged) {
-            numberOfMerges++;
-            Log.info(getClass(), "Merged class %s into %s.", clazz.toSourceString(),
-                targetClass.toSourceString());
-          } else {
-            Log.info(getClass(), "Aborted merge for class %s into %s.",
-                clazz.toSourceString(), targetClass.toSourceString());
-          }
-        }
-      }
-    }
-    if (Log.ENABLED) {
-      Log.debug(getClass(), "Merged %d classes.", numberOfMerges);
-    }
-    return renamedMembersLense.build(application.dexItemFactory, graphLense);
-  }
-
-  private class ClassMerger {
-
-    private static final String CONSTRUCTOR_NAME = "constructor";
-
-    private final DexClass source;
-    private final DexClass target;
-    private final Map<DexEncodedMethod, DexEncodedMethod> deferredRenamings = new HashMap<>();
-    private boolean abortMerge = false;
-
-    private ClassMerger(DexClass source, DexClass target) {
-      this.source = source;
-      this.target = target;
-    }
-
-    public boolean merge() {
-      if (source.getEnclosingMethod() != null || !source.getInnerClasses().isEmpty()
-          || target.getEnclosingMethod() != null || !target.getInnerClasses().isEmpty()) {
-        // TODO(herhut): Consider supporting merging of inner-class attributes.
-        return false;
-      }
-      // Merge the class [clazz] into [targetClass] by adding all methods to
-      // targetClass that are not currently contained.
-      // Step 1: Merge methods
-      Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
-      addAll(existingMethods, target.directMethods(), MethodSignatureEquivalence.get());
-      addAll(existingMethods, target.virtualMethods(), MethodSignatureEquivalence.get());
-      Collection<DexEncodedMethod> mergedDirectMethods = mergeItems(
-          Iterators.transform(Iterators.forArray(source.directMethods()), this::renameConstructors),
-          target.directMethods(),
-          MethodSignatureEquivalence.get(),
-          existingMethods,
-          this::renameMethod
-      );
-      Iterator<DexEncodedMethod> methods = Iterators.forArray(source.virtualMethods());
-      if (source.accessFlags.isInterface()) {
-        // If merging an interface, only merge methods that are not otherwise defined in the
-        // target class.
-        methods = Iterators.transform(methods, this::filterShadowedInterfaceMethods);
-      }
-      Collection<DexEncodedMethod> mergedVirtualMethods = mergeItems(
-          methods,
-          target.virtualMethods(),
-          MethodSignatureEquivalence.get(),
-          existingMethods,
-          this::abortOnNonAbstract);
-      if (abortMerge) {
-        return false;
-      }
-      // Step 2: Merge fields
-      Set<Wrapper<DexField>> existingFields = new HashSet<>();
-      addAll(existingFields, target.instanceFields(), FieldSignatureEquivalence.get());
-      addAll(existingFields, target.staticFields(), FieldSignatureEquivalence.get());
-      Collection<DexEncodedField> mergedStaticFields = mergeItems(
-          Iterators.forArray(source.staticFields()),
-          target.staticFields(),
-          FieldSignatureEquivalence.get(),
-          existingFields,
-          this::renameField);
-      Collection<DexEncodedField> mergedInstanceFields = mergeItems(
-          Iterators.forArray(source.instanceFields()),
-          target.instanceFields(),
-          FieldSignatureEquivalence.get(),
-          existingFields,
-          this::renameField);
-      // Step 3: Merge interfaces
-      Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
-      // Now destructively update the class.
-      // Step 1: Update supertype or fix interfaces.
-      if (source.isInterface()) {
-        interfaces.remove(source.type);
-      } else {
-        assert !target.isInterface();
-        target.superType = source.superType;
-      }
-      target.interfaces = interfaces.isEmpty()
-          ? DexTypeList.empty()
-          : new DexTypeList(interfaces.toArray(new DexType[interfaces.size()]));
-      // Step 2: replace fields and methods.
-      target.setDirectMethods(mergedDirectMethods
-          .toArray(new DexEncodedMethod[mergedDirectMethods.size()]));
-      target.setVirtualMethods(mergedVirtualMethods
-          .toArray(new DexEncodedMethod[mergedVirtualMethods.size()]));
-      target.setStaticFields(mergedStaticFields
-          .toArray(new DexEncodedField[mergedStaticFields.size()]));
-      target.setInstanceFields(mergedInstanceFields
-          .toArray(new DexEncodedField[mergedInstanceFields.size()]));
-      // Step 3: Unlink old class to ease tree shaking.
-      source.superType = application.dexItemFactory.objectType;
-      source.setDirectMethods(null);
-      source.setVirtualMethods(null);
-      source.setInstanceFields(null);
-      source.setStaticFields(null);
-      source.interfaces = DexTypeList.empty();
-      // Step 4: Record merging.
-      mergedClasses.put(source.type, target.type);
-      // Step 5: Make deferred renamings final.
-      deferredRenamings.forEach((from, to) -> renamedMembersLense.map(from.method, to.method));
-      return true;
-    }
-
-    private DexEncodedMethod filterShadowedInterfaceMethods(DexEncodedMethod m) {
-      DexEncodedMethod actual = appInfo.resolveMethod(target.type, m.method).asSingleTarget();
-      assert actual != null;
-      if (actual != m) {
-        // We will drop a method here, so record it as a potential renaming.
-        deferredRenamings.put(m, actual);
-        return null;
-      }
-      // We will keep the method, so the class better be abstract.
-      assert target.accessFlags.isAbstract();
-      return m;
-    }
-
-    private <T extends KeyedDexItem<S>, S extends PresortedComparable<S>> void addAll(
-        Collection<Wrapper<S>> collection, T[] items, Equivalence<S> equivalence) {
-      for (T item : items) {
-        collection.add(equivalence.wrap(item.getKey()));
-      }
-    }
-
-    private <T> Set<T> mergeArrays(T[] one, T[] other) {
-      Set<T> merged = new LinkedHashSet<>();
-      Collections.addAll(merged, one);
-      Collections.addAll(merged, other);
-      return merged;
-    }
-
-    private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> Collection<S> mergeItems(
-        Iterator<S> fromItems,
-        S[] toItems,
-        Equivalence<T> equivalence,
-        Set<Wrapper<T>> existing,
-        BiFunction<S, S, S> onConflict) {
-      HashMap<Wrapper<T>, S> methods = new HashMap<>();
-      // First add everything from the target class. These items are not preprocessed.
-      for (S item : toItems) {
-        methods.put(equivalence.wrap(item.getKey()), item);
-      }
-      // Now add the new methods, resolving shadowing.
-      addNonShadowed(fromItems, methods, equivalence, existing, onConflict);
-      return methods.values();
-    }
-
-    private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> void addNonShadowed(
-        Iterator<S> items,
-        HashMap<Wrapper<T>, S> map,
-        Equivalence<T> equivalence,
-        Set<Wrapper<T>> existing,
-        BiFunction<S, S, S> onConflict) {
-      while (items.hasNext()) {
-        S item = items.next();
-        if (item == null) {
-          // This item was filtered out by a preprocessing.
-          continue;
-        }
-        Wrapper<T> wrapped = equivalence.wrap(item.getKey());
-        if (existing.contains(wrapped)) {
-          S resolved = onConflict.apply(map.get(wrapped), item);
-          wrapped = equivalence.wrap(resolved.getKey());
-          map.put(wrapped, resolved);
-        } else {
-          map.put(wrapped, item);
-        }
-      }
-    }
-
-    private DexString makeMergedName(String nameString, DexType holder) {
-      return application.dexItemFactory
-          .createString(nameString + "$" + holder.toSourceString().replace('.', '$'));
-    }
-
-    private DexEncodedMethod abortOnNonAbstract(DexEncodedMethod existing,
-        DexEncodedMethod method) {
-      if (existing == null) {
-        // This is a conflict between a static and virtual method. Abort.
-        abortMerge = true;
-        return method;
-      }
-      // Ignore if we merge in an abstract method or if we override a bridge method that would
-      // bridge to the superclasses method.
-      if (method.accessFlags.isAbstract()) {
-        // We make a method disappear here, so record the renaming so that calls to the previous
-        // target get forwarded properly.
-        deferredRenamings.put(method, existing);
-        return existing;
-      } else if (existing.accessFlags.isBridge()) {
-        InvokeSingleTargetExtractor extractor = new InvokeSingleTargetExtractor();
-        existing.getCode().registerCodeReferences(extractor);
-        if (extractor.getTarget() != method.method) {
-          abortMerge = true;
-        }
-        return method;
-      } else {
-        abortMerge = true;
-        return existing;
-      }
-    }
-
-    private DexEncodedMethod renameConstructors(DexEncodedMethod method) {
-      // Only rename instance initializers.
-      if (!method.isInstanceInitializer()) {
-        return method;
-      }
-      DexType holder = method.method.holder;
-      DexEncodedMethod result = method
-          .toRenamedMethod(makeMergedName(CONSTRUCTOR_NAME, holder), application.dexItemFactory);
-      result.markForceInline();
-      deferredRenamings.put(method, result);
-      // Renamed constructors turn into ordinary private functions. They can be private, as
-      // they are only references from their direct subclass, which they were merged into.
-      result.accessFlags.unsetConstructor();
-      result.accessFlags.unsetPublic();
-      result.accessFlags.unsetProtected();
-      result.accessFlags.setPrivate();
-      return result;
-    }
-
-    private DexEncodedMethod renameMethod(DexEncodedMethod existing, DexEncodedMethod method) {
-      // We cannot handle renaming static initializers yet and constructors should have been
-      // renamed already.
-      assert !method.accessFlags.isConstructor();
-      DexType holder = method.method.holder;
-      String name = method.method.name.toSourceString();
-      DexEncodedMethod result = method
-          .toRenamedMethod(makeMergedName(name, holder), application.dexItemFactory);
-      renamedMembersLense.map(method.method, result.method);
-      return result;
-    }
-
-    private DexEncodedField renameField(DexEncodedField existing, DexEncodedField field) {
-      DexString oldName = field.field.name;
-      DexType holder = field.field.clazz;
-      DexEncodedField result = field
-          .toRenamedField(makeMergedName(oldName.toSourceString(), holder),
-              application.dexItemFactory);
-      renamedMembersLense.map(field.field, result.field);
-      return result;
-    }
-  }
-
-  private class TreeFixer {
-
-    private final Builder lense = GraphLense.builder();
-    Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
-
-    private GraphLense fixupTypeReferences(GraphLense graphLense) {
-      // Globally substitute merged class types in protos and holders.
-      for (DexProgramClass clazz : appInfo.classes()) {
-        clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
-        clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
-        clazz.setVirtualMethods(removeDupes(clazz.virtualMethods()));
-        clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
-        clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
-      }
-      // Record type renamings so instanceof and checkcast checks are also fixed.
-      for (DexType type : mergedClasses.keySet()) {
-        DexType fixed = fixupType(type);
-        lense.map(type, fixed);
-      }
-      return lense.build(application.dexItemFactory, graphLense);
-    }
-
-    private DexEncodedMethod[] removeDupes(DexEncodedMethod[] methods) {
-      if (methods == null) {
-        return null;
-      }
-      Map<DexMethod, DexEncodedMethod> filtered = new IdentityHashMap<>();
-      for (DexEncodedMethod method : methods) {
-        DexEncodedMethod previous = filtered.put(method.method, method);
-        if (previous != null) {
-          if (!previous.accessFlags.isBridge()) {
-            if (!method.accessFlags.isBridge()) {
-              throw new CompilationError(
-                  "Class merging produced invalid result on: " + previous.toSourceString());
-            } else {
-              filtered.put(previous.method, previous);
-            }
-          }
-        }
-      }
-      if (filtered.size() == methods.length) {
-        return methods;
-      }
-      return filtered.values().toArray(new DexEncodedMethod[filtered.size()]);
-    }
-
-    private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
-      if (methods == null) {
-        return null;
-      }
-      for (int i = 0; i < methods.length; i++) {
-        DexEncodedMethod encodedMethod = methods[i];
-        DexMethod method = encodedMethod.method;
-        DexProto newProto = getUpdatedProto(method.proto);
-        DexType newHolder = fixupType(method.holder);
-        DexMethod newMethod = application.dexItemFactory.createMethod(newHolder, newProto,
-            method.name);
-        if (newMethod != encodedMethod.method) {
-          lense.map(encodedMethod.method, newMethod);
-          methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
-        }
-      }
-      return methods;
-    }
-
-    private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
-      if (fields == null) {
-        return null;
-      }
-      for (int i = 0; i < fields.length; i++) {
-        DexEncodedField encodedField = fields[i];
-        DexField field = encodedField.field;
-        DexType newType = fixupType(field.type);
-        DexType newHolder = fixupType(field.clazz);
-        DexField newField = application.dexItemFactory.createField(newHolder, newType, field.name);
-        if (newField != encodedField.field) {
-          lense.map(encodedField.field, newField);
-          fields[i] = encodedField.toTypeSubstitutedField(newField);
-        }
-      }
-      return fields;
-    }
-
-    private DexProto getUpdatedProto(DexProto proto) {
-      DexProto result = protoFixupCache.get(proto);
-      if (result == null) {
-        DexType returnType = fixupType(proto.returnType);
-        DexType[] arguments = fixupTypes(proto.parameters.values);
-        result = application.dexItemFactory.createProto(returnType, arguments);
-        protoFixupCache.put(proto, result);
-      }
-      return result;
-    }
-
-    private DexType fixupType(DexType type) {
-      if (type.isArrayType()) {
-        DexType base = type.toBaseType(application.dexItemFactory);
-        DexType fixed = fixupType(base);
-        if (base == fixed) {
-          return type;
-        } else {
-          return type.replaceBaseType(fixed, application.dexItemFactory);
-        }
-      }
-      while (mergedClasses.containsKey(type)) {
-        type = mergedClasses.get(type);
-      }
-      return type;
-    }
-
-    private DexType[] fixupTypes(DexType[] types) {
-      DexType[] result = new DexType[types.length];
-      for (int i = 0; i < result.length; i++) {
-        result[i] = fixupType(types[i]);
-      }
-      return result;
-    }
-  }
-
-  private static class CollisionDetector {
-
-    private static final int NOT_FOUND = 1 << (Integer.SIZE - 1);
-
-    // TODO(herhut): Maybe cache seenPositions for target classes.
-    private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
-    private final Reference2IntMap<DexProto> targetProtoCache;
-    private final Reference2IntMap<DexProto> sourceProtoCache;
-    private final DexType source, target;
-    private final Collection<DexMethod> invokes;
-    private final Map<DexType, DexType> substituions;
-
-    private CollisionDetector(DexType source, DexType target, Collection<DexMethod> invokes,
-        Map<DexType, DexType> substitutions) {
-      this.source = source;
-      this.target = target;
-      this.invokes = invokes;
-      this.substituions = substitutions;
-      this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
-      this.targetProtoCache.defaultReturnValue(NOT_FOUND);
-      this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
-      this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
-    }
-
-    boolean mayCollide() {
-      fillSeenPositions(invokes);
-      // If the type is not used in methods at all, there cannot be any conflict.
-      if (seenPositions.isEmpty()) {
-        return false;
-      }
-      for (DexMethod method : invokes) {
-        Int2IntMap positionsMap = seenPositions.get(method.name);
-        if (positionsMap != null) {
-          int arity = method.getArity();
-          int previous = positionsMap.get(arity);
-          if (previous != NOT_FOUND) {
-            assert previous != 0;
-            int positions = computePositionsFor(method.proto, source, sourceProtoCache,
-                substituions);
-            if ((positions & previous) != 0) {
-              return true;
-            }
-          }
-        }
-      }
-      return false;
-    }
-
-    private void fillSeenPositions(Collection<DexMethod> invokes) {
-      for (DexMethod method : invokes) {
-        DexType[] parameters = method.proto.parameters.values;
-        int arity = parameters.length;
-        int positions = computePositionsFor(method.proto, target, targetProtoCache, substituions);
-        if (positions != 0) {
-          Int2IntMap positionsMap =
-              seenPositions.computeIfAbsent(method.name, k -> {
-                Int2IntMap result = new Int2IntOpenHashMap();
-                result.defaultReturnValue(NOT_FOUND);
-                return result;
-              });
-          int value = 0;
-          int previous = positionsMap.get(arity);
-          if (previous != NOT_FOUND) {
-            value = previous;
-          }
-          value |= positions;
-          positionsMap.put(arity, value);
-        }
-      }
-
-    }
-
-    private int computePositionsFor(DexProto proto, DexType type,
-        Reference2IntMap<DexProto> cache, Map<DexType, DexType> substitutions) {
-      int result = cache.getInt(proto);
-      if (result != NOT_FOUND) {
-        return result;
-      }
-      result = 0;
-      int bitsUsed = 0;
-      int accumulator = 0;
-      for (DexType aType : proto.parameters.values) {
-        if (substitutions != null) {
-          // Substitute the type with the already merged class to estimate what it will
-          // look like.
-          while (substitutions.containsKey(aType)) {
-            aType = substitutions.get(aType);
-          }
-        }
-        accumulator <<= 1;
-        bitsUsed++;
-        if (aType == type) {
-          accumulator |= 1;
-        }
-        // Handle overflow on 31 bit boundary.
-        if (bitsUsed == Integer.SIZE - 1) {
-          result |= accumulator;
-          accumulator = 0;
-          bitsUsed = 0;
-        }
-      }
-      // We also take the return type into account for potential conflicts.
-      DexType returnType = proto.returnType;
-      if (substitutions != null) {
-        while (substitutions.containsKey(returnType)) {
-          returnType = substitutions.get(returnType);
-        }
-      }
-      accumulator <<= 1;
-      if (returnType == type) {
-        accumulator |= 1;
-      }
-      result |= accumulator;
-      cache.put(proto, result);
-      return result;
-    }
-  }
-
-  public Collection<DexType> getRemovedClasses() {
-    return Collections.unmodifiableCollection(mergedClasses.keySet());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
new file mode 100644
index 0000000..4b57209
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -0,0 +1,1075 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DefaultUseRegistry;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graph.GraphLense.Builder;
+import com.android.tools.r8.graph.KeyedDexItem;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.graph.PresortedComparable;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
+import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.optimize.InvokeSingleTargetExtractor;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterators;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+/**
+ * Merges Supertypes with a single implementation into their single subtype.
+ *
+ * <p>A common use-case for this is to merge an interface into its single implementation.
+ *
+ * <p>The class merger only fixes the structure of the graph but leaves the actual instructions
+ * untouched. Fixup of instructions is deferred via a {@link GraphLense} to the IR building phase.
+ */
+public class VerticalClassMerger {
+
+  private final DexApplication application;
+  private final AppInfoWithLiveness appInfo;
+  private final GraphLense graphLense;
+  private final Map<DexType, DexType> mergedClasses = new HashMap<>();
+  private final Timing timing;
+  private Collection<DexMethod> invokes;
+
+  public VerticalClassMerger(
+      DexApplication application,
+      AppInfoWithLiveness appInfo,
+      GraphLense graphLense,
+      Timing timing) {
+    this.application = application;
+    this.appInfo = appInfo;
+    this.graphLense = graphLense;
+    this.timing = timing;
+  }
+
+  // Returns a set of types that must not be merged into other types.
+  private Set<DexType> getPinnedTypes(Iterable<DexProgramClass> classes) {
+    Set<DexType> pinnedTypes = new HashSet<>();
+    for (DexProgramClass clazz : classes) {
+      for (DexEncodedMethod method : clazz.methods()) {
+        // TODO(christofferqa): Remove the invariant that the graph lense should not modify any
+        // methods from the sets alwaysInline and noSideEffects (see use of assertNotModifiedBy-
+        // Lense).
+        if (appInfo.alwaysInline.contains(method) || appInfo.noSideEffects.containsKey(method)) {
+          DexClass other = appInfo.definitionFor(method.method.proto.returnType);
+          if (other != null && other.isProgramClass()) {
+            // If we were to merge [other] into its sub class, then we would implicitly change the
+            // signature of this method, and therefore break the invariant.
+            pinnedTypes.add(other.type);
+          }
+          for (DexType parameterType : method.method.proto.parameters.values) {
+            other = appInfo.definitionFor(parameterType);
+            if (other != null && other.isProgramClass()) {
+              // If we were to merge [other] into its sub class, then we would implicitly change the
+              // signature of this method, and therefore break the invariant.
+              pinnedTypes.add(other.type);
+            }
+          }
+        }
+
+        // Avoid merging two types if this could remove a NoSuchMethodError, as illustrated by the
+        // following example. (Alternatively, it would be possible to merge A and B and rewrite the
+        // "invoke-super A.m" instruction into "invoke-super Object.m" to preserve the error. This
+        // situation should generally not occur in practice, though.)
+        //
+        //   class A {}
+        //   class B extends A {
+        //     public void m() {}
+        //   }
+        //   class C extends A {
+        //     public void m() {
+        //       invoke-super "A.m" <- should yield NoSuchMethodError, cannot merge A and B
+        //     }
+        //   }
+        if (!method.isStaticMethod()) {
+          method.registerCodeReferences(
+              new DefaultUseRegistry() {
+                @Override
+                public boolean registerInvokeSuper(DexMethod target) {
+                  DexClass targetClass = appInfo.definitionFor(target.getHolder());
+                  if (targetClass != null
+                      && targetClass.isProgramClass()
+                      && targetClass.lookupVirtualMethod(target) == null) {
+                    pinnedTypes.add(target.getHolder());
+                  }
+                  return true;
+                }
+              });
+        }
+      }
+    }
+    return pinnedTypes;
+  }
+
+  private boolean isMergeCandidate(DexProgramClass clazz, Set<DexType> pinnedTypes) {
+    if (appInfo.instantiatedTypes.contains(clazz.type)
+        || appInfo.isPinned(clazz.type)
+        || pinnedTypes.contains(clazz.type)) {
+      return false;
+    }
+    DexType singleSubtype = clazz.type.getSingleSubtype();
+    if (singleSubtype == null) {
+      // TODO(christofferqa): Even if [clazz] has multiple subtypes, we could still merge it into
+      // its subclass if [clazz] is not live. This should only be done, though, if it does not
+      // lead to members being duplicated.
+      return false;
+    }
+    DexClass targetClass = appInfo.definitionFor(singleSubtype);
+    if (mergeMayLeadToIllegalAccesses(clazz, targetClass)) {
+      return false;
+    }
+    for (DexEncodedField field : clazz.fields()) {
+      if (appInfo.isPinned(field.field)) {
+        return false;
+      }
+    }
+    for (DexEncodedMethod method : clazz.methods()) {
+      if (appInfo.isPinned(method.method)) {
+        return false;
+      }
+      if (method.isInstanceInitializer() && disallowInlining(method)) {
+        // Cannot guarantee that markForceInline() will work.
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean mergeMayLeadToIllegalAccesses(DexClass clazz, DexClass singleSubclass) {
+    if (clazz.type.isSamePackage(singleSubclass.type)) {
+      return false;
+    }
+    // TODO(christofferqa): To merge [clazz] into a class from another package we need to ensure:
+    // (A) All accesses to [clazz] and its members from inside the current package of [clazz] will
+    //     continue to work. This is guaranteed if [clazz] is public and all members of [clazz] are
+    //     either private or public.
+    // (B) All accesses from [clazz] to classes or members from the current package of [clazz] will
+    //     continue to work. This is guaranteed if the methods of [clazz] do not access any private
+    //     or protected classes or members from the current package of [clazz].
+    return true;
+  }
+
+  private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
+      Equivalence<DexMethod> equivalence) {
+    DexClass definition = appInfo.definitionFor(method.holder);
+    if (definition != null && definition.isProgramClass()) {
+      set.add(equivalence.wrap(method));
+    }
+  }
+
+  private Collection<DexMethod> getInvokes() {
+    if (invokes == null) {
+      // Collect all reachable methods that are not within a library class. Those defined on
+      // library classes are known not to have program classes in their signature.
+      // Also filter methods that only use types from library classes in their signatures. We
+      // know that those won't conflict.
+      Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
+      Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+      appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
+      invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
+          .collect(Collectors.toList());
+    }
+    return invokes;
+  }
+
+  private boolean isProgramClass(DexType type) {
+    if (type.isArrayType()) {
+      type = type.toBaseType(appInfo.dexItemFactory);
+    }
+    if (type.isClassType()) {
+      DexClass clazz = appInfo.definitionFor(type);
+      if (clazz != null && clazz.isProgramClass()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean removeNonProgram(DexMethod dexMethod) {
+    for (DexType type : dexMethod.proto.parameters.values) {
+      if (isProgramClass(type)) {
+        return true;
+      }
+    }
+    return isProgramClass(dexMethod.proto.returnType);
+  }
+
+  public GraphLense run() {
+    timing.begin("merge");
+    GraphLense mergingGraphLense = mergeClasses(graphLense);
+    timing.end();
+    timing.begin("fixup");
+    GraphLense result = new TreeFixer().fixupTypeReferences(mergingGraphLense);
+    timing.end();
+    return result;
+  }
+
+  private void addAncestorsToWorklist(
+      DexProgramClass clazz, Deque<DexProgramClass> worklist, Set<DexProgramClass> seenBefore) {
+    if (seenBefore.contains(clazz)) {
+      return;
+    }
+
+    worklist.addFirst(clazz);
+
+    // Add super classes to worklist.
+    if (clazz.superType != null) {
+      DexClass definition = appInfo.definitionFor(clazz.superType);
+      if (definition != null && definition.isProgramClass()) {
+        addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+      }
+    }
+
+    // Add super interfaces to worklist.
+    for (DexType interfaceType : clazz.interfaces.values) {
+      DexClass definition = appInfo.definitionFor(interfaceType);
+      if (definition != null && definition.isProgramClass()) {
+        addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+      }
+    }
+  }
+
+  private GraphLense mergeClasses(GraphLense graphLense) {
+    Iterable<DexProgramClass> classes = application.classesWithDeterministicOrder();
+    Deque<DexProgramClass> worklist = new ArrayDeque<>();
+    Set<DexProgramClass> seenBefore = new HashSet<>();
+
+    int numberOfMerges = 0;
+
+    // Types that are pinned (in addition to those where appInfo.isPinned returns true).
+    Set<DexType> pinnedTypes = getPinnedTypes(classes);
+
+    // The resulting graph lense that should be used after class merging.
+    VerticalClassMergerGraphLense.Builder renamedMembersLense =
+        new VerticalClassMergerGraphLense.Builder();
+
+    Iterator<DexProgramClass> classIterator = classes.iterator();
+
+    // Visit the program classes in a top-down order according to the class hierarchy.
+    while (classIterator.hasNext() || !worklist.isEmpty()) {
+      if (worklist.isEmpty()) {
+        // Add the ancestors of this class (including the class itself) to the worklist in such a
+        // way that all super types of the class come before the class itself.
+        addAncestorsToWorklist(classIterator.next(), worklist, seenBefore);
+        if (worklist.isEmpty()) {
+          continue;
+        }
+      }
+
+      DexProgramClass clazz = worklist.removeFirst();
+      if (!seenBefore.add(clazz) || !isMergeCandidate(clazz, pinnedTypes)) {
+        continue;
+      }
+
+      DexClass targetClass = appInfo.definitionFor(clazz.type.getSingleSubtype());
+      assert !mergedClasses.containsKey(targetClass.type);
+      if (appInfo.isPinned(targetClass.type)) {
+        // We have to keep the target class intact, so we cannot merge it.
+        continue;
+      }
+      if (clazz.hasClassInitializer() && targetClass.hasClassInitializer()) {
+        // TODO(herhut): Handle class initializers.
+        if (Log.ENABLED) {
+          Log.info(
+              getClass(),
+              "Cannot merge %s into %s due to static initializers.",
+              clazz.toSourceString(),
+              targetClass.toSourceString());
+        }
+        continue;
+      }
+      // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+      // to the super class.
+      if (fieldResolutionMayChange(clazz, targetClass)) {
+        continue;
+      }
+      // Guard against the case where we have two methods that may get the same signature
+      // if we replace types. This is rare, so we approximate and err on the safe side here.
+      if (new CollisionDetector(clazz.type, targetClass.type, getInvokes(), mergedClasses)
+          .mayCollide()) {
+        if (Log.ENABLED) {
+          Log.info(
+              getClass(),
+              "Cannot merge %s into %s due to conflict.",
+              clazz.toSourceString(),
+              targetClass.toSourceString());
+        }
+        continue;
+      }
+      ClassMerger merger = new ClassMerger(clazz, targetClass);
+      boolean merged = merger.merge();
+      if (merged) {
+        // Commit the changes to the graph lense.
+        renamedMembersLense.merge(merger.getRenamings());
+        // Do not allow merging the resulting class into its subclass.
+        // TODO(christofferqa): Get rid of this limitation.
+        pinnedTypes.add(targetClass.type);
+      }
+      if (Log.ENABLED) {
+        if (merged) {
+          numberOfMerges++;
+          Log.info(
+              getClass(),
+              "Merged class %s into %s.",
+              clazz.toSourceString(),
+              targetClass.toSourceString());
+        } else {
+          Log.info(
+              getClass(),
+              "Aborted merge for class %s into %s.",
+              clazz.toSourceString(),
+              targetClass.toSourceString());
+        }
+      }
+    }
+    if (Log.ENABLED) {
+      Log.debug(getClass(), "Merged %d classes.", numberOfMerges);
+    }
+    return renamedMembersLense.build(graphLense);
+  }
+
+  private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+    if (source.type == target.superType) {
+      // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+      // Target implements an interface that declares a static final field f, this should yield an
+      // IncompatibleClassChangeError.
+      // TODO(christofferqa): In the following we only check if a static field from an interface
+      // shadows an instance field from [source]. We could actually check if there is an iget/iput
+      // instruction whose resolution would be affected by the merge. The situation where a static
+      // field shadows an instance field is probably not widespread in practice, though.
+      FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+      Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+      for (DexType interfaceType : target.interfaces.values) {
+        DexClass clazz = appInfo.definitionFor(interfaceType);
+        for (DexEncodedField staticField : clazz.staticFields()) {
+          staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.field));
+        }
+      }
+      for (DexEncodedField instanceField : source.instanceFields()) {
+        if (staticFieldsInInterfacesOfTarget.contains(equivalence.wrap(instanceField.field))) {
+          // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+          // interface would now hit an instance field from [source], so that an IncompatibleClass-
+          // ChangeError would no longer be thrown. Abort merge.
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private class ClassMerger {
+
+    private static final String CONSTRUCTOR_NAME = "constructor";
+
+    private final DexClass source;
+    private final DexClass target;
+    private final VerticalClassMergerGraphLense.Builder deferredRenamings =
+        new VerticalClassMergerGraphLense.Builder();
+    private boolean abortMerge = false;
+
+    private ClassMerger(DexClass source, DexClass target) {
+      this.source = source;
+      this.target = target;
+    }
+
+    public boolean merge() {
+      if (source.getEnclosingMethod() != null || !source.getInnerClasses().isEmpty()
+          || target.getEnclosingMethod() != null || !target.getInnerClasses().isEmpty()) {
+        // TODO(herhut): Consider supporting merging of inner-class attributes.
+        return false;
+      }
+      // Merge the class [clazz] into [targetClass] by adding all methods to
+      // targetClass that are not currently contained.
+      // Step 1: Merge methods
+      Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
+      addAll(existingMethods, target.methods(), MethodSignatureEquivalence.get());
+
+      List<DexEncodedMethod> directMethods = new ArrayList<>();
+      for (DexEncodedMethod directMethod : source.directMethods()) {
+        if (directMethod.isInstanceInitializer()) {
+          directMethods.add(renameConstructor(directMethod));
+        } else {
+          directMethods.add(directMethod);
+        }
+      }
+
+      List<DexEncodedMethod> virtualMethods = new ArrayList<>();
+      for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+        DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
+        if (shadowedBy != null) {
+          if (virtualMethod.accessFlags.isAbstract()) {
+            // Remove abstract/interface methods that are shadowed.
+            deferredRenamings.map(virtualMethod.method, shadowedBy.method);
+            continue;
+          }
+        } else {
+          // The method is shadowed. If it is abstract, we can simply move it to the subclass.
+          // Non-abstract methods are handled below (they cannot simply be moved to the subclass as
+          // a virtual method, because they might be the target of an invoke-super instruction).
+          if (virtualMethod.accessFlags.isAbstract()) {
+            DexEncodedMethod resultingVirtualMethod =
+                renameMethod(virtualMethod, target.type, false);
+            deferredRenamings.map(virtualMethod.method, resultingVirtualMethod.method);
+            virtualMethods.add(resultingVirtualMethod);
+            continue;
+          }
+        }
+
+        // This virtual method could be called directly from a sub class via an invoke-super
+        // instruction. Therefore, we translate this virtual method into a direct method, such that
+        // relevant invoke-super instructions can be rewritten into invoke-direct instructions.
+        DexEncodedMethod resultingDirectMethod = renameMethod(virtualMethod, target.type, true);
+        makePrivate(resultingDirectMethod);
+        directMethods.add(resultingDirectMethod);
+
+        // Record that invoke-super instructions in the target class should be redirected to the
+        // newly created direct method.
+        redirectSuperCallsInTarget(virtualMethod.method, resultingDirectMethod.method);
+
+        if (shadowedBy == null) {
+          // In addition to the newly added direct method, create a virtual method such that we do
+          // not accidentally remove the method from the interface of this class.
+          // Note that this method is added independently of whether it will actually be used. If
+          // it turns out that the method is never used, it will be removed by the final round
+          // of tree shaking.
+          shadowedBy = buildBridgeMethod(virtualMethod, resultingDirectMethod.method);
+          virtualMethods.add(shadowedBy);
+        }
+
+        deferredRenamings.map(virtualMethod.method, shadowedBy.method);
+      }
+
+      Collection<DexEncodedMethod> mergedDirectMethods =
+          mergeItems(
+              directMethods.iterator(),
+              target.directMethods(),
+              MethodSignatureEquivalence.get(),
+              existingMethods,
+              (existing, method) -> {
+                DexEncodedMethod renamedMethod = renameMethod(method, target.type, true);
+                deferredRenamings.map(method.method, renamedMethod.method);
+                return renamedMethod;
+              });
+      Collection<DexEncodedMethod> mergedVirtualMethods =
+          mergeItems(
+              virtualMethods.iterator(),
+              target.virtualMethods(),
+              MethodSignatureEquivalence.get(),
+              existingMethods,
+              this::abortOnNonAbstract);
+      if (abortMerge) {
+        return false;
+      }
+      // Step 2: Merge fields
+      Set<Wrapper<DexField>> existingFields = new HashSet<>();
+      addAll(existingFields, target.fields(), FieldSignatureEquivalence.get());
+      Collection<DexEncodedField> mergedStaticFields = mergeItems(
+          Iterators.forArray(source.staticFields()),
+          target.staticFields(),
+          FieldSignatureEquivalence.get(),
+          existingFields,
+          this::renameField);
+      Collection<DexEncodedField> mergedInstanceFields = mergeItems(
+          Iterators.forArray(source.instanceFields()),
+          target.instanceFields(),
+          FieldSignatureEquivalence.get(),
+          existingFields,
+          this::renameField);
+      // Step 3: Merge interfaces
+      Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
+      // Now destructively update the class.
+      // Step 1: Update supertype or fix interfaces.
+      if (source.isInterface()) {
+        interfaces.remove(source.type);
+      } else {
+        assert !target.isInterface();
+        target.superType = source.superType;
+      }
+      target.interfaces = interfaces.isEmpty()
+          ? DexTypeList.empty()
+          : new DexTypeList(interfaces.toArray(new DexType[interfaces.size()]));
+      // Step 2: replace fields and methods.
+      target.setDirectMethods(mergedDirectMethods
+          .toArray(new DexEncodedMethod[mergedDirectMethods.size()]));
+      target.setVirtualMethods(mergedVirtualMethods
+          .toArray(new DexEncodedMethod[mergedVirtualMethods.size()]));
+      target.setStaticFields(mergedStaticFields
+          .toArray(new DexEncodedField[mergedStaticFields.size()]));
+      target.setInstanceFields(mergedInstanceFields
+          .toArray(new DexEncodedField[mergedInstanceFields.size()]));
+      // Step 3: Unlink old class to ease tree shaking.
+      source.superType = application.dexItemFactory.objectType;
+      source.setDirectMethods(null);
+      source.setVirtualMethods(null);
+      source.setInstanceFields(null);
+      source.setStaticFields(null);
+      source.interfaces = DexTypeList.empty();
+      // Step 4: Record merging.
+      mergedClasses.put(source.type, target.type);
+      return true;
+    }
+
+    public VerticalClassMergerGraphLense.Builder getRenamings() {
+      return deferredRenamings;
+    }
+
+    private void redirectSuperCallsInTarget(DexMethod oldTarget, DexMethod newTarget) {
+      // If we merge class B into class C, and class C contains an invocation super.m(), then it
+      // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
+      // C.m$B denotes the direct method that has been created in C for B.m). In particular, there
+      // might be an instruction "invoke-super A.m()" in C that resolves to B.m at runtime (A is
+      // a superclass of B), which also needs to be rewritten to "invoke-direct C.m$B()".
+      //
+      // We handle this by adding a mapping for [target] and all of its supertypes.
+      DexClass holder = target;
+      while (holder != null && holder.isProgramClass()) {
+        DexMethod signatureInHolder =
+            application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
+        // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
+        if (resolutionSucceeds(signatureInHolder)) {
+          deferredRenamings.mapVirtualMethodToDirectInType(
+              signatureInHolder, newTarget, target.type);
+          holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
+        } else {
+          break;
+        }
+      }
+    }
+
+    private boolean resolutionSucceeds(DexMethod targetMethod) {
+      DexClass enclosingClass = appInfo.definitionFor(targetMethod.holder);
+      return enclosingClass != null
+          && (enclosingClass.lookupVirtualMethod(targetMethod) != null
+              || appInfo.lookupSuperTarget(targetMethod, enclosingClass.type) != null);
+    }
+
+    private DexEncodedMethod buildBridgeMethod(
+        DexEncodedMethod signature, DexMethod invocationTarget) {
+      DexType holder = target.type;
+      DexProto proto = invocationTarget.proto;
+      DexString name = signature.method.name;
+      MethodAccessFlags accessFlags = signature.accessFlags.copy();
+      accessFlags.setBridge();
+      accessFlags.setSynthetic();
+      accessFlags.unsetAbstract();
+      return new DexEncodedMethod(
+          application.dexItemFactory.createMethod(holder, proto, name),
+          accessFlags,
+          DexAnnotationSet.empty(),
+          ParameterAnnotationsList.empty(),
+          new SynthesizedCode(
+              new ForwardMethodSourceCode(holder, proto, holder, invocationTarget, Type.DIRECT),
+              registry -> registry.registerInvokeDirect(invocationTarget)));
+    }
+
+    // Returns the method that shadows the given method, or null if method is not shadowed.
+    private DexEncodedMethod findMethodInTarget(DexEncodedMethod method) {
+      DexEncodedMethod actual = appInfo.resolveMethod(target.type, method.method).asSingleTarget();
+      if (actual == null) {
+        // May happen in case of missing classes.
+        abortMerge = true;
+        return null;
+      }
+      if (actual != method) {
+        return actual;
+      }
+      // We will keep the method, so the class better be abstract if there is no implementation.
+      assert !method.accessFlags.isAbstract() || target.accessFlags.isAbstract();
+      return null;
+    }
+
+    private <T extends KeyedDexItem<S>, S extends PresortedComparable<S>> void addAll(
+        Collection<Wrapper<S>> collection, Iterable<T> items, Equivalence<S> equivalence) {
+      for (T item : items) {
+        collection.add(equivalence.wrap(item.getKey()));
+      }
+    }
+
+    private <T> Set<T> mergeArrays(T[] one, T[] other) {
+      Set<T> merged = new LinkedHashSet<>();
+      Collections.addAll(merged, one);
+      Collections.addAll(merged, other);
+      return merged;
+    }
+
+    private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> Collection<S> mergeItems(
+        Iterator<S> fromItems,
+        S[] toItems,
+        Equivalence<T> equivalence,
+        Set<Wrapper<T>> existing,
+        BiFunction<S, S, S> onConflict) {
+      HashMap<Wrapper<T>, S> methods = new HashMap<>();
+      // First add everything from the target class. These items are not preprocessed.
+      for (S item : toItems) {
+        methods.put(equivalence.wrap(item.getKey()), item);
+      }
+      // Now add the new methods, resolving shadowing.
+      addNonShadowed(fromItems, methods, equivalence, existing, onConflict);
+      return methods.values();
+    }
+
+    private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> void addNonShadowed(
+        Iterator<S> items,
+        HashMap<Wrapper<T>, S> map,
+        Equivalence<T> equivalence,
+        Set<Wrapper<T>> existing,
+        BiFunction<S, S, S> onConflict) {
+      while (items.hasNext()) {
+        S item = items.next();
+        if (item == null) {
+          // This item was filtered out by a preprocessing.
+          continue;
+        }
+        Wrapper<T> wrapped = equivalence.wrap(item.getKey());
+        if (existing.contains(wrapped)) {
+          S resolved = onConflict.apply(map.get(wrapped), item);
+          wrapped = equivalence.wrap(resolved.getKey());
+          map.put(wrapped, resolved);
+        } else {
+          map.put(wrapped, item);
+        }
+      }
+    }
+
+    // Note that names returned by this function are not necessarily unique. However, class merging
+    // is aborted if it turns out that the returned name is not unique.
+    // TODO(christofferqa): Write a test that checks that class merging is aborted if this function
+    // generates a name that is not unique.
+    private DexString getFreshName(String nameString, DexType holder) {
+      String freshName = nameString + "$" + holder.toSourceString().replace('.', '$');
+      return application.dexItemFactory.createString(freshName);
+    }
+
+    private DexEncodedMethod abortOnNonAbstract(DexEncodedMethod existing,
+        DexEncodedMethod method) {
+      // Ignore if we override a bridge method that would bridge to the superclasses method.
+      if (existing != null && existing.accessFlags.isBridge()) {
+        InvokeSingleTargetExtractor extractor = new InvokeSingleTargetExtractor();
+        existing.getCode().registerCodeReferences(extractor);
+        if (extractor.getTarget() == method.method) {
+          return method;
+        }
+      }
+
+      // Abstract shadowed methods are removed prior to calling mergeItems.
+      assert !method.accessFlags.isAbstract();
+
+      // If [existing] is null, there is a conflict between a static and virtual method. Otherwise,
+      // there is a non-abstract method that is shadowed. We translate virtual shadowed methods into
+      // direct methods with a fresh name, so this situation should never happen. We currently get
+      // in this situation if getFreshName returns a name that is not unique, though.
+      abortMerge = true;
+      return method;
+    }
+
+    private DexEncodedMethod renameConstructor(DexEncodedMethod method) {
+      assert method.isInstanceInitializer();
+      DexType holder = method.method.holder;
+      DexEncodedMethod result =
+          method.toRenamedMethod(
+              getFreshName(CONSTRUCTOR_NAME, holder), application.dexItemFactory);
+      result.markForceInline();
+      deferredRenamings.map(method.method, result.method);
+      // Renamed constructors turn into ordinary private functions. They can be private, as
+      // they are only references from their direct subclass, which they were merged into.
+      result.accessFlags.unsetConstructor();
+      makePrivate(result);
+      return result;
+    }
+
+    private DexEncodedMethod renameMethod(
+        DexEncodedMethod method, DexType newHolder, boolean useFreshName) {
+      // We cannot handle renaming static initializers yet and constructors should have been
+      // renamed already.
+      assert !method.accessFlags.isConstructor();
+      DexType oldHolder = method.method.holder;
+      DexString oldName = method.method.name;
+      DexString newName =
+          useFreshName ? getFreshName(oldName.toSourceString(), oldHolder) : oldName;
+      DexMethod newSignature =
+          application.dexItemFactory.createMethod(newHolder, method.method.proto, newName);
+      return method.toTypeSubstitutedMethod(newSignature);
+    }
+
+    private DexEncodedField renameField(DexEncodedField existing, DexEncodedField field) {
+      DexString oldName = field.field.name;
+      DexType oldHolder = field.field.clazz;
+      DexString newName = getFreshName(oldName.toSourceString(), oldHolder);
+      DexField newSignature =
+          application.dexItemFactory.createField(target.type, field.field.type, newName);
+      DexEncodedField result = field.toTypeSubstitutedField(newSignature);
+      deferredRenamings.map(field.field, result.field);
+      return result;
+    }
+  }
+
+  private static void makePrivate(DexEncodedMethod method) {
+    assert !method.accessFlags.isAbstract();
+    method.accessFlags.unsetPublic();
+    method.accessFlags.unsetProtected();
+    method.accessFlags.setPrivate();
+  }
+
+  private class TreeFixer {
+
+    private final Builder lense = GraphLense.builder();
+    Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
+
+    private GraphLense fixupTypeReferences(GraphLense graphLense) {
+      // Globally substitute merged class types in protos and holders.
+      for (DexProgramClass clazz : appInfo.classes()) {
+        clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
+        clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
+        clazz.setVirtualMethods(removeDupes(clazz.virtualMethods()));
+        clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
+        clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
+      }
+      // Record type renamings so instanceof and checkcast checks are also fixed.
+      for (DexType type : mergedClasses.keySet()) {
+        DexType fixed = fixupType(type);
+        lense.map(type, fixed);
+      }
+      return lense.build(application.dexItemFactory, graphLense);
+    }
+
+    private DexEncodedMethod[] removeDupes(DexEncodedMethod[] methods) {
+      if (methods == null) {
+        return null;
+      }
+      Map<DexMethod, DexEncodedMethod> filtered = new IdentityHashMap<>();
+      for (DexEncodedMethod method : methods) {
+        DexEncodedMethod previous = filtered.put(method.method, method);
+        if (previous != null) {
+          if (!previous.accessFlags.isBridge()) {
+            if (!method.accessFlags.isBridge()) {
+              throw new CompilationError(
+                  "Class merging produced invalid result on: " + previous.toSourceString());
+            } else {
+              filtered.put(previous.method, previous);
+            }
+          }
+        }
+      }
+      if (filtered.size() == methods.length) {
+        return methods;
+      }
+      return filtered.values().toArray(new DexEncodedMethod[filtered.size()]);
+    }
+
+    private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
+      if (methods == null) {
+        return null;
+      }
+      for (int i = 0; i < methods.length; i++) {
+        DexEncodedMethod encodedMethod = methods[i];
+        DexMethod method = encodedMethod.method;
+        DexProto newProto = getUpdatedProto(method.proto);
+        DexType newHolder = fixupType(method.holder);
+        DexMethod newMethod = application.dexItemFactory.createMethod(newHolder, newProto,
+            method.name);
+        if (newMethod != encodedMethod.method) {
+          lense.map(encodedMethod.method, newMethod);
+          methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+        }
+      }
+      return methods;
+    }
+
+    private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
+      if (fields == null) {
+        return null;
+      }
+      for (int i = 0; i < fields.length; i++) {
+        DexEncodedField encodedField = fields[i];
+        DexField field = encodedField.field;
+        DexType newType = fixupType(field.type);
+        DexType newHolder = fixupType(field.clazz);
+        DexField newField = application.dexItemFactory.createField(newHolder, newType, field.name);
+        if (newField != encodedField.field) {
+          lense.map(encodedField.field, newField);
+          fields[i] = encodedField.toTypeSubstitutedField(newField);
+        }
+      }
+      return fields;
+    }
+
+    private DexProto getUpdatedProto(DexProto proto) {
+      DexProto result = protoFixupCache.get(proto);
+      if (result == null) {
+        DexType returnType = fixupType(proto.returnType);
+        DexType[] arguments = fixupTypes(proto.parameters.values);
+        result = application.dexItemFactory.createProto(returnType, arguments);
+        protoFixupCache.put(proto, result);
+      }
+      return result;
+    }
+
+    private DexType fixupType(DexType type) {
+      if (type.isArrayType()) {
+        DexType base = type.toBaseType(application.dexItemFactory);
+        DexType fixed = fixupType(base);
+        if (base == fixed) {
+          return type;
+        } else {
+          return type.replaceBaseType(fixed, application.dexItemFactory);
+        }
+      }
+      while (mergedClasses.containsKey(type)) {
+        type = mergedClasses.get(type);
+      }
+      return type;
+    }
+
+    private DexType[] fixupTypes(DexType[] types) {
+      DexType[] result = new DexType[types.length];
+      for (int i = 0; i < result.length; i++) {
+        result[i] = fixupType(types[i]);
+      }
+      return result;
+    }
+  }
+
+  private static class CollisionDetector {
+
+    private static final int NOT_FOUND = 1 << (Integer.SIZE - 1);
+
+    // TODO(herhut): Maybe cache seenPositions for target classes.
+    private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
+    private final Reference2IntMap<DexProto> targetProtoCache;
+    private final Reference2IntMap<DexProto> sourceProtoCache;
+    private final DexType source, target;
+    private final Collection<DexMethod> invokes;
+    private final Map<DexType, DexType> substituions;
+
+    private CollisionDetector(DexType source, DexType target, Collection<DexMethod> invokes,
+        Map<DexType, DexType> substitutions) {
+      this.source = source;
+      this.target = target;
+      this.invokes = invokes;
+      this.substituions = substitutions;
+      this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+      this.targetProtoCache.defaultReturnValue(NOT_FOUND);
+      this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+      this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
+    }
+
+    boolean mayCollide() {
+      fillSeenPositions(invokes);
+      // If the type is not used in methods at all, there cannot be any conflict.
+      if (seenPositions.isEmpty()) {
+        return false;
+      }
+      for (DexMethod method : invokes) {
+        Int2IntMap positionsMap = seenPositions.get(method.name);
+        if (positionsMap != null) {
+          int arity = method.getArity();
+          int previous = positionsMap.get(arity);
+          if (previous != NOT_FOUND) {
+            assert previous != 0;
+            int positions = computePositionsFor(method.proto, source, sourceProtoCache,
+                substituions);
+            if ((positions & previous) != 0) {
+              return true;
+            }
+          }
+        }
+      }
+      return false;
+    }
+
+    private void fillSeenPositions(Collection<DexMethod> invokes) {
+      for (DexMethod method : invokes) {
+        DexType[] parameters = method.proto.parameters.values;
+        int arity = parameters.length;
+        int positions = computePositionsFor(method.proto, target, targetProtoCache, substituions);
+        if (positions != 0) {
+          Int2IntMap positionsMap =
+              seenPositions.computeIfAbsent(method.name, k -> {
+                Int2IntMap result = new Int2IntOpenHashMap();
+                result.defaultReturnValue(NOT_FOUND);
+                return result;
+              });
+          int value = 0;
+          int previous = positionsMap.get(arity);
+          if (previous != NOT_FOUND) {
+            value = previous;
+          }
+          value |= positions;
+          positionsMap.put(arity, value);
+        }
+      }
+
+    }
+
+    private int computePositionsFor(DexProto proto, DexType type,
+        Reference2IntMap<DexProto> cache, Map<DexType, DexType> substitutions) {
+      int result = cache.getInt(proto);
+      if (result != NOT_FOUND) {
+        return result;
+      }
+      result = 0;
+      int bitsUsed = 0;
+      int accumulator = 0;
+      for (DexType aType : proto.parameters.values) {
+        if (substitutions != null) {
+          // Substitute the type with the already merged class to estimate what it will
+          // look like.
+          while (substitutions.containsKey(aType)) {
+            aType = substitutions.get(aType);
+          }
+        }
+        accumulator <<= 1;
+        bitsUsed++;
+        if (aType == type) {
+          accumulator |= 1;
+        }
+        // Handle overflow on 31 bit boundary.
+        if (bitsUsed == Integer.SIZE - 1) {
+          result |= accumulator;
+          accumulator = 0;
+          bitsUsed = 0;
+        }
+      }
+      // We also take the return type into account for potential conflicts.
+      DexType returnType = proto.returnType;
+      if (substitutions != null) {
+        while (substitutions.containsKey(returnType)) {
+          returnType = substitutions.get(returnType);
+        }
+      }
+      accumulator <<= 1;
+      if (returnType == type) {
+        accumulator |= 1;
+      }
+      result |= accumulator;
+      cache.put(proto, result);
+      return result;
+    }
+  }
+
+  private static boolean disallowInlining(DexEncodedMethod method) {
+    // TODO(christofferqa): Determine the situations where markForceInline() may fail, and ensure
+    // that we always return true here in these cases.
+    MethodInlineDecision registry = new MethodInlineDecision();
+    method.getCode().registerCodeReferences(registry);
+    return registry.isInliningDisallowed();
+  }
+
+  private static class MethodInlineDecision extends UseRegistry {
+    private boolean disallowInlining = false;
+
+    public boolean isInliningDisallowed() {
+      return disallowInlining;
+    }
+
+    private boolean allowInlining() {
+      return true;
+    }
+
+    private boolean disallowInlining() {
+      disallowInlining = true;
+      return true;
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return disallowInlining();
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      return disallowInlining();
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      return allowInlining();
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      return allowInlining();
+    }
+  }
+
+  public Collection<DexType> getRemovedClasses() {
+    return Collections.unmodifiableCollection(mergedClasses.keySet());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
new file mode 100644
index 0000000..ea57d1d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
@@ -0,0 +1,175 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+// This graph lense is instantiated during vertical class merging. The graph lense is context
+// sensitive in the enclosing class of a given invoke *and* the type of the invoke (e.g., invoke-
+// super vs invoke-virtual). This is illustrated by the following example.
+//
+// public class A {
+//   public void m() { ... }
+// }
+// public class B extends A {
+//   @Override
+//   public void m() { invoke-super A.m(); ... }
+//
+//   public void m2() { invoke-virtual A.m(); ... }
+// }
+//
+// Vertical class merging will merge class A into class B. Since class B already has a method with
+// the signature "void B.m()", the method A.m will be given a fresh name and moved to class B.
+// During this process, the method corresponding to A.m will be made private such that it can be
+// called via an invoke-direct instruction.
+//
+// For the invocation "invoke-super A.m()" in B.m, this graph lense will return the newly created,
+// private method corresponding to A.m (that is now in B.m with a fresh name), such that the
+// invocation will hit the same implementation as the original super.m() call.
+//
+// For the invocation "invoke-virtual A.m()" in B.m2, this graph lense will return the method B.m.
+public class VerticalClassMergerGraphLense extends GraphLense {
+  private final GraphLense previousLense;
+
+  private final Map<DexField, DexField> fieldMap;
+  private final Map<DexMethod, DexMethod> methodMap;
+  private final Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps;
+
+  public VerticalClassMergerGraphLense(
+      Map<DexField, DexField> fieldMap,
+      Map<DexMethod, DexMethod> methodMap,
+      Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps,
+      GraphLense previousLense) {
+    this.previousLense = previousLense;
+    this.fieldMap = fieldMap;
+    this.methodMap = methodMap;
+    this.contextualVirtualToDirectMethodMaps = contextualVirtualToDirectMethodMaps;
+  }
+
+  @Override
+  public DexType lookupType(DexType type) {
+    return previousLense.lookupType(type);
+  }
+
+  @Override
+  public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+    assert isContextFreeForMethod(method) || (context != null && type != null);
+    DexMethod previous = previousLense.lookupMethod(method, context, type);
+    if (type == Type.SUPER) {
+      Map<DexMethod, DexMethod> virtualToDirectMethodMap =
+          contextualVirtualToDirectMethodMaps.get(context.method.holder);
+      if (virtualToDirectMethodMap != null) {
+        DexMethod directMethod = virtualToDirectMethodMap.get(previous);
+        if (directMethod != null) {
+          return directMethod;
+        }
+      }
+    }
+    return methodMap.getOrDefault(previous, previous);
+  }
+
+  @Override
+  public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+    ImmutableSet.Builder<DexMethod> builder = ImmutableSet.builder();
+    for (DexMethod previous : previousLense.lookupMethodInAllContexts(method)) {
+      builder.add(methodMap.getOrDefault(previous, previous));
+      for (Map<DexMethod, DexMethod> virtualToDirectMethodMap :
+          contextualVirtualToDirectMethodMaps.values()) {
+        DexMethod directMethod = virtualToDirectMethodMap.get(previous);
+        if (directMethod != null) {
+          builder.add(directMethod);
+        }
+      }
+    }
+    return builder.build();
+  }
+
+  @Override
+  public DexField lookupField(DexField field) {
+    DexField previous = previousLense.lookupField(field);
+    return fieldMap.getOrDefault(previous, previous);
+  }
+
+  @Override
+  public boolean isContextFreeForMethods() {
+    return contextualVirtualToDirectMethodMaps.isEmpty() && previousLense.isContextFreeForMethods();
+  }
+
+  @Override
+  public boolean isContextFreeForMethod(DexMethod method) {
+    if (!previousLense.isContextFreeForMethod(method)) {
+      return false;
+    }
+    DexMethod previous = previousLense.lookupMethod(method);
+    for (Map<DexMethod, DexMethod> virtualToDirectMethodMap :
+        contextualVirtualToDirectMethodMaps.values()) {
+      if (virtualToDirectMethodMap.containsKey(previous)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public static class Builder {
+
+    private final ImmutableMap.Builder<DexField, DexField> fieldMapBuilder = ImmutableMap.builder();
+    private final ImmutableMap.Builder<DexMethod, DexMethod> methodMapBuilder =
+        ImmutableMap.builder();
+    private final Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps =
+        new HashMap<>();
+
+    public Builder() {}
+
+    public GraphLense build(GraphLense previousLense) {
+      Map<DexField, DexField> fieldMap = fieldMapBuilder.build();
+      Map<DexMethod, DexMethod> methodMap = methodMapBuilder.build();
+      if (fieldMap.isEmpty()
+          && methodMap.isEmpty()
+          && contextualVirtualToDirectMethodMaps.isEmpty()) {
+        return previousLense;
+      }
+      return new VerticalClassMergerGraphLense(
+          fieldMap, methodMap, contextualVirtualToDirectMethodMaps, previousLense);
+    }
+
+    public void map(DexField from, DexField to) {
+      fieldMapBuilder.put(from, to);
+    }
+
+    public void map(DexMethod from, DexMethod to) {
+      methodMapBuilder.put(from, to);
+    }
+
+    public void mapVirtualMethodToDirectInType(DexMethod from, DexMethod to, DexType type) {
+      Map<DexMethod, DexMethod> virtualToDirectMethodMap =
+          contextualVirtualToDirectMethodMaps.computeIfAbsent(type, key -> new HashMap<>());
+      virtualToDirectMethodMap.put(from, to);
+    }
+
+    public void merge(VerticalClassMergerGraphLense.Builder builder) {
+      fieldMapBuilder.putAll(builder.fieldMapBuilder.build());
+      methodMapBuilder.putAll(builder.methodMapBuilder.build());
+      for (DexType context : builder.contextualVirtualToDirectMethodMaps.keySet()) {
+        Map<DexMethod, DexMethod> current = contextualVirtualToDirectMethodMaps.get(context);
+        Map<DexMethod, DexMethod> other = builder.contextualVirtualToDirectMethodMaps.get(context);
+        if (current != null) {
+          current.putAll(other);
+        } else {
+          contextualVirtualToDirectMethodMaps.put(context, other);
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 37aa208..a611105 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -4,8 +4,6 @@
 package com.android.tools.r8.utils;
 
 import static com.android.tools.r8.utils.FileUtils.isArchive;
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
 
 import com.android.tools.r8.DataDirectoryResource;
 import com.android.tools.r8.DataEntryResource;
@@ -22,8 +20,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -49,22 +46,22 @@
   private List<ProgramResource> readArchive() throws IOException {
     List<ProgramResource> dexResources = new ArrayList<>();
     List<ProgramResource> classResources = new ArrayList<>();
-    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
+    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
         try (InputStream stream = zipFile.getInputStream(entry)) {
-          Path name = Paths.get(entry.getName());
-          Origin entryOrigin = new ArchiveEntryOrigin(entry.getName(), origin);
+          String name = entry.getName();
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
           if (archive.matchesFile(name)) {
-            if (isDexFile(name)) {
+            if (ZipUtils.isDexFile(name)) {
               if (!ignoreDexInArchive) {
                 ProgramResource resource =
                     OneShotByteResource.create(
                         Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
                 dexResources.add(resource);
               }
-            } else if (isClassFile(name)) {
+            } else if (ZipUtils.isClassFile(name)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
               ProgramResource resource =
                   OneShotByteResource.create(
@@ -105,11 +102,11 @@
 
   @Override
   public void accept(Visitor resourceBrowser) throws ResourceException {
-    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
+    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
-        Path name = Paths.get(entry.getName());
+        String name = entry.getName();
         if (archive.matchesFile(name) && !isProgramResourceName(name)) {
           if (entry.isDirectory()) {
             resourceBrowser.visit(DataDirectoryResource.fromZip(zipFile, entry));
@@ -127,7 +124,7 @@
     }
   }
 
-  private boolean isProgramResourceName(Path name) {
-    return isClassFile(name) || (isDexFile(name) && !ignoreDexInArchive);
+  private boolean isProgramResourceName(String name) {
+    return ZipUtils.isClassFile(name) || (ZipUtils.isDexFile(name) && !ignoreDexInArchive);
   }
 }
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 fa045e6..a846ea7 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -341,22 +341,35 @@
     }
   }
 
-  // Guess class descriptor from location of the class file.
+  /**
+   * Guess class descriptor from location of the class file on the file system
+   *
+   * @param name Path of the file to convert to the corresponding descriptor
+   * @return java class descriptor
+   */
   public static String guessTypeDescriptor(Path name) {
-    return guessTypeDescriptor(name.toString());
+    String fileName = name.toString();
+    if (File.separatorChar != '/') {
+      fileName = fileName.replace(File.separatorChar, '/');
+    }
+    return guessTypeDescriptor(fileName);
   }
 
-  // Guess class descriptor from location of the class file.
+  /**
+   * Guess class descriptor from location of the class file. This method assumes that the
+   * name uses '/' as the separator. Therefore, this should not be the name of a file
+   * on a file system.
+   *
+   * @param name the location of the class file to convert to descriptor
+   * @return java class descriptor
+   */
   public static String guessTypeDescriptor(String name) {
     assert name != null;
     assert name.endsWith(CLASS_EXTENSION) :
         "Name " + name + " must have " + CLASS_EXTENSION + " suffix";
-    String fileName =
-        File.separatorChar == '/' ? name.toString() :
-            name.toString().replace(File.separatorChar, '/');
-    String descriptor = fileName.substring(0, fileName.length() - CLASS_EXTENSION.length());
+    String descriptor = name.substring(0, name.length() - CLASS_EXTENSION.length());
     if (descriptor.contains(".")) {
-      throw new CompilationError("Unexpected class file name: " + fileName);
+      throw new CompilationError("Unexpected class file name: " + name);
     }
     return 'L' + descriptor + ';';
   }
diff --git a/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
index f933b42..0c6cc14 100644
--- a/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
@@ -6,12 +6,11 @@
 import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.shaking.FilteredClassPath;
 import java.io.IOException;
-import java.nio.file.Paths;
 
 // Internal filtered class-file provider.
 class FilteredArchiveClassFileProvider extends ArchiveClassFileProvider {
 
   FilteredArchiveClassFileProvider(FilteredClassPath archive) throws IOException {
-    super(archive.getPath(), entry -> archive.matchesFile(Paths.get(entry)));
+    super(archive.getPath(), entry -> archive.matchesFile(entry));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index b938596..f10ff69 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNaming;
+import com.android.tools.r8.naming.ClassNaming.Builder;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -187,6 +188,22 @@
     }
   }
 
+  // We will be remapping positional debug events and collect them as MappedPositions.
+  private static class MappedPosition {
+    private final DexMethod method;
+    private final int originalLine;
+    private final Position caller;
+    private final int obfuscatedLine;
+
+    private MappedPosition(
+        DexMethod method, int originalLine, Position caller, int obfuscatedLine) {
+      this.method = method;
+      this.originalLine = originalLine;
+      this.caller = caller;
+      this.obfuscatedLine = obfuscatedLine;
+    }
+  }
+
   public static ClassNameMapper run(
       DexApplication application, NamingLens namingLens, boolean identityMapping) {
     IdentityHashMap<DexString, List<DexProgramClass>> classesOfFiles = new IdentityHashMap<>();
@@ -199,25 +216,8 @@
         continue;
       }
 
-      // Group methods by name
       IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
-          new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
-      clazz.forEachMethod(
-          method -> {
-            // Add method only if renamed or contains positions.
-            if (namingLens.lookupName(method.method) != method.method.name
-                || doesContainPositions(method)) {
-              methodsByName.compute(
-                  method.method.name,
-                  (name, methods) -> {
-                    if (methods == null) {
-                      methods = new ArrayList<>();
-                    }
-                    methods.add(method);
-                    return methods;
-                  });
-            }
-          });
+          groupMethodsByName(namingLens, clazz);
 
       // At this point we don't know if we really need to add this class to the builder.
       // It depends on whether any methods/fields are renamed or some methods contain positions.
@@ -230,24 +230,11 @@
                       DescriptorUtils.descriptorToJavaType(renamedClassName.toString()),
                       clazz.toString()));
 
-      // We do know we need to create a ClassNaming.Builder if the class itself had been renamed.
-      if (!clazz.toString().equals(renamedClassName.toString())) {
-        // Not using return value, it's registered in classNameMapperBuilder
-        onDemandClassNamingBuilder.get();
-      }
+      // If the class is renamed add it to the classNamingBuilder.
+      addClassToClassNaming(clazz, renamedClassName, onDemandClassNamingBuilder);
 
       // First transfer renamed fields to classNamingBuilder.
-      clazz.forEachField(
-          dexEncodedField -> {
-            DexField dexField = dexEncodedField.field;
-            DexString renamedName = namingLens.lookupName(dexField);
-            if (renamedName != dexField.name) {
-              FieldSignature signature =
-                  new FieldSignature(dexField.name.toString(), dexField.type.toString());
-              MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
-              onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
-            }
-          });
+      addFieldsToClassNaming(namingLens, clazz, onDemandClassNamingBuilder);
 
       // Then process the methods.
       for (List<DexEncodedMethod> methods : methodsByName.values()) {
@@ -256,47 +243,13 @@
           // deterministic behaviour: the algorithm will assign new line numbers in this order.
           // Methods with different names can share the same line numbers, that's why they don't
           // need to be sorted.
-          methods.sort(
-              (lhs, rhs) -> {
-                // Sort by startline, then DexEncodedMethod.slowCompare.
-                // Use startLine = 0 if no debuginfo.
-                Code lhsCode = lhs.getCode();
-                Code rhsCode = rhs.getCode();
-                DexCode lhsDexCode =
-                    lhsCode == null || !lhsCode.isDexCode() ? null : lhsCode.asDexCode();
-                DexCode rhsDexCode =
-                    rhsCode == null || !rhsCode.isDexCode() ? null : rhsCode.asDexCode();
-                DexDebugInfo lhsDebugInfo = lhsDexCode == null ? null : lhsDexCode.getDebugInfo();
-                DexDebugInfo rhsDebugInfo = rhsDexCode == null ? null : rhsDexCode.getDebugInfo();
-                int lhsStartLine = lhsDebugInfo == null ? 0 : lhsDebugInfo.startLine;
-                int rhsStartLine = rhsDebugInfo == null ? 0 : rhsDebugInfo.startLine;
-                int startLineDiff = lhsStartLine - rhsStartLine;
-                if (startLineDiff != 0) return startLineDiff;
-                return DexEncodedMethod.slowCompare(lhs, rhs);
-              });
+          sortMethods(methods);
         }
 
         PositionRemapper positionRemapper =
             identityMapping ? new IdentityPositionRemapper() : new OptimizingPositionRemapper();
 
         for (DexEncodedMethod method : methods) {
-
-          // We will be remapping positional debug events and collect them as MappedPositions.
-          class MappedPosition {
-            private final DexMethod method;
-            private final int originalLine;
-            private final Position caller;
-            private final int obfuscatedLine;
-
-            private MappedPosition(
-                DexMethod method, int originalLine, Position caller, int obfuscatedLine) {
-              this.method = method;
-              this.originalLine = originalLine;
-              this.caller = caller;
-              this.obfuscatedLine = obfuscatedLine;
-            }
-          }
-
           List<MappedPosition> mappedPositions = new ArrayList<>();
 
           if (doesContainPositions(method)) {
@@ -429,6 +382,75 @@
     return classNameMapperBuilder.build();
   }
 
+  // Sort by startline, then DexEncodedMethod.slowCompare.
+  // Use startLine = 0 if no debuginfo.
+  private static void sortMethods(List<DexEncodedMethod> methods) {
+    methods.sort(
+        (lhs, rhs) -> {
+          Code lhsCode = lhs.getCode();
+          Code rhsCode = rhs.getCode();
+          DexCode lhsDexCode =
+              lhsCode == null || !lhsCode.isDexCode() ? null : lhsCode.asDexCode();
+          DexCode rhsDexCode =
+              rhsCode == null || !rhsCode.isDexCode() ? null : rhsCode.asDexCode();
+          DexDebugInfo lhsDebugInfo = lhsDexCode == null ? null : lhsDexCode.getDebugInfo();
+          DexDebugInfo rhsDebugInfo = rhsDexCode == null ? null : rhsDexCode.getDebugInfo();
+          int lhsStartLine = lhsDebugInfo == null ? 0 : lhsDebugInfo.startLine;
+          int rhsStartLine = rhsDebugInfo == null ? 0 : rhsDebugInfo.startLine;
+          int startLineDiff = lhsStartLine - rhsStartLine;
+          if (startLineDiff != 0) return startLineDiff;
+          return DexEncodedMethod.slowCompare(lhs, rhs);
+        });
+  }
+
+  @SuppressWarnings("ReturnValueIgnored")
+  private static void addClassToClassNaming(DexProgramClass clazz, DexString renamedClassName,
+      Supplier<Builder> onDemandClassNamingBuilder) {
+    // We do know we need to create a ClassNaming.Builder if the class itself had been renamed.
+    if (!clazz.toString().equals(renamedClassName.toString())) {
+      // Not using return value, it's registered in classNameMapperBuilder
+      onDemandClassNamingBuilder.get();
+    }
+  }
+
+  private static void addFieldsToClassNaming(NamingLens namingLens, DexProgramClass clazz,
+      Supplier<Builder> onDemandClassNamingBuilder) {
+    clazz.forEachField(
+        dexEncodedField -> {
+          DexField dexField = dexEncodedField.field;
+          DexString renamedName = namingLens.lookupName(dexField);
+          if (renamedName != dexField.name) {
+            FieldSignature signature =
+                new FieldSignature(dexField.name.toString(), dexField.type.toString());
+            MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
+            onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
+          }
+        });
+  }
+
+  private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByName(
+      NamingLens namingLens, DexProgramClass clazz) {
+    IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
+        new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
+    clazz.forEachMethod(
+        method -> {
+          // Add method only if renamed or contains positions.
+          if (namingLens.lookupName(method.method) != method.method.name
+              || doesContainPositions(method)) {
+            methodsByName.compute(
+                method.method.name,
+                (name, methods) -> {
+                  if (methods == null) {
+                    methods = new ArrayList<>();
+                  }
+                  methods.add(method);
+                  return methods;
+                });
+          }
+        });
+    return methodsByName;
+  }
+
   private static boolean doesContainPositions(DexEncodedMethod method) {
     Code code = method.getCode();
     if (code == null || !code.isDexCode()) {
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 1b032e5..fd50583 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.MODULE_INFO_CLASS;
+
 import com.android.tools.r8.errors.CompilationError;
 import com.google.common.io.ByteStreams;
 import java.io.File;
@@ -10,6 +14,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Enumeration;
@@ -27,7 +32,7 @@
   }
 
   public static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
-    try (ZipFile zipFile = new ZipFile(zipFileStr)) {
+    try (ZipFile zipFile = new ZipFile(zipFileStr, StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
@@ -91,4 +96,17 @@
     stream.write(content);
     stream.closeEntry();
   }
+
+  public static boolean isDexFile(String entry) {
+    String name = entry.toLowerCase();
+    return name.endsWith(DEX_EXTENSION);
+  }
+
+  public static boolean isClassFile(String entry) {
+    String name = entry.toLowerCase();
+    if (name.endsWith(MODULE_INFO_CLASS)) {
+      return false;
+    }
+    return name.endsWith(CLASS_EXTENSION);
+  }
 }
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
index ad31005..23dc7e0 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
@@ -22,6 +22,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -387,11 +388,12 @@
     for (Path file : files) {
       if (isArchive(file)) {
         Origin zipOrigin = new PathOrigin(file);
-        ZipInputStream zip = new ZipInputStream(Files.newInputStream(file));
+        ZipInputStream zip = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8);
         ZipEntry entry;
         while (null != (entry = zip.getNextEntry())) {
-          if (isClassFile(Paths.get(entry.getName()))) {
-            Origin origin = new ArchiveEntryOrigin(entry.getName(), zipOrigin);
+          String name = entry.getName();
+          if (isClassFile(name)) {
+            Origin origin = new ArchiveEntryOrigin(name, zipOrigin);
             classfiles.add(new ClassFileContent(origin, readBytes(zip)));
           }
         }
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
index 7230f55..7bce10a 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
@@ -22,6 +22,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -378,11 +379,12 @@
     for (Path file : files) {
       if (isArchive(file)) {
         Origin zipOrigin = new PathOrigin(file);
-        ZipInputStream zip = new ZipInputStream(Files.newInputStream(file));
+        ZipInputStream zip = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8);
         ZipEntry entry;
         while (null != (entry = zip.getNextEntry())) {
-          if (isClassFile(Paths.get(entry.getName()))) {
-            Origin origin = new ArchiveEntryOrigin(entry.getName(), zipOrigin);
+          String name = entry.getName();
+          if (isClassFile(name)) {
+            Origin origin = new ArchiveEntryOrigin(name, zipOrigin);
             classfiles.add(new ClassFileContent(origin, readBytes(zip)));
           }
         }
diff --git a/src/test/examples/barray/BArray.java b/src/test/examples/barray/BArray.java
index 0ca6505..26ac5f8 100644
--- a/src/test/examples/barray/BArray.java
+++ b/src/test/examples/barray/BArray.java
@@ -6,20 +6,59 @@
 public class BArray {
 
   public static void main(String[] args) {
+    System.out.println("null boolean: " + readNullBooleanArray());
+    System.out.println("null byte: " + readNullByteArray());
+    System.out.println("boolean: " + readBooleanArray(writeBooleanArray(args)));
+    System.out.println("byte: " + readByteArray(writeByteArray(args)));
+  }
+
+  public static boolean readNullBooleanArray() {
     boolean[] boolArray = null;
+    try {
+      return boolArray[0] || boolArray[1];
+    } catch (Throwable e) {
+      return true;
+    }
+  }
+
+  public static byte readNullByteArray() {
     byte[] byteArray = null;
-    boolean bool;
-    byte bits;
     try {
-      bool = boolArray[0] || boolArray[1];
+      return byteArray[0];
     } catch (Throwable e) {
-      bool = true;
+      return 42;
     }
+  }
+
+  public static boolean[] writeBooleanArray(String[] args) {
+    boolean[] array = new boolean[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = args[i].length() == 42;
+    }
+    return array;
+  }
+
+  public static byte[] writeByteArray(String[] args) {
+    byte[] array = new byte[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = (byte) args[i].length();
+    }
+    return array;
+  }
+
+  public static boolean readBooleanArray(boolean[] boolArray) {
     try {
-      bits = byteArray[0];
+      return boolArray[0] || boolArray[1];
     } catch (Throwable e) {
-      bits = 42;
+      return true;
     }
-    System.out.println("bits " + bits + " and bool " + bool);
+  }
+
+  public static byte readByteArray(byte[] byteArray) {
+    try {
+      return byteArray[0];
+    } catch (Throwable e) {
+      return 42;
+    }
   }
 }
diff --git a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
new file mode 100644
index 0000000..223ff37
--- /dev/null
+++ b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class ConflictingInterfaceSignaturesTest {
+
+  public static void main(String[] args) {
+    A a = new InterfaceImpl();
+    a.foo();
+
+    B b = new InterfaceImpl();
+    b.foo();
+  }
+
+  public interface A {
+    void foo();
+  }
+
+  public interface B {
+    void foo();
+  }
+
+  public static final class InterfaceImpl implements A, B {
+
+    @Override
+    public void foo() {
+      System.out.println("In foo on InterfaceImpl");
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/ExceptionTest.java b/src/test/examples/classmerging/ExceptionTest.java
new file mode 100644
index 0000000..28442f3
--- /dev/null
+++ b/src/test/examples/classmerging/ExceptionTest.java
@@ -0,0 +1,56 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class ExceptionTest {
+  public static void main(String[] args) {
+    // The following will lead to a catch handler for ExceptionA, which is merged into ExceptionB.
+    try {
+      doSomethingThatMightThrowExceptionB();
+      doSomethingThatMightThrowException2();
+    } catch (ExceptionB exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    } catch (ExceptionA exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    } catch (Exception2 exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    } catch (Exception1 exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    }
+  }
+
+  private static void doSomethingThatMightThrowExceptionB() throws ExceptionB {
+    throw new ExceptionB("Ouch!");
+  }
+
+  private static void doSomethingThatMightThrowException2() throws Exception2 {
+    throw new Exception2("Ouch!");
+  }
+
+  // Will be merged into ExceptionB when class merging is enabled.
+  public static class ExceptionA extends Exception {
+    public ExceptionA(String message) {
+      super(message);
+    }
+  }
+
+  public static class ExceptionB extends ExceptionA {
+    public ExceptionB(String message) {
+      super(message);
+    }
+  }
+
+  public static class Exception1 extends Exception {
+    public Exception1(String message) {
+      super(message);
+    }
+  }
+
+  public static class Exception2 extends Exception1 {
+    public Exception2(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/SimpleInterface.java b/src/test/examples/classmerging/SimpleInterface.java
new file mode 100644
index 0000000..e8ebcab
--- /dev/null
+++ b/src/test/examples/classmerging/SimpleInterface.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public interface SimpleInterface {
+  void foo();
+}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
new file mode 100644
index 0000000..04b5386
--- /dev/null
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2018, 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 classmerging;
+
+import classmerging.pkg.SimpleInterfaceImplRetriever;
+
+public class SimpleInterfaceAccessTest {
+  public static void main(String[] args) {
+    // It is not possible to merge the interface SimpleInterface into SimpleInterfaceImpl, since
+    // this would lead to an illegal class access here.
+    SimpleInterface obj = SimpleInterfaceImplRetriever.getSimpleInterfaceImpl();
+    obj.foo();
+  }
+}
diff --git a/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java b/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
index c12804c..c09b11a 100644
--- a/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
+++ b/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
@@ -7,6 +7,9 @@
 
   @Override
   public String referencedMethod() {
-    return "From sub: " + super.referencedMethod();
+    System.out.println("In referencedMethod on SubClassThatReferencesSuperMethod");
+    System.out.println("Calling referencedMethod on SuperClassWithReferencedMethod with super");
+    System.out.println("Got: " + super.referencedMethod());
+    return "SubClassThatReferencesSuperMethod.referencedMethod()";
   }
 }
diff --git a/src/test/examples/classmerging/SuperCallRewritingTest.java b/src/test/examples/classmerging/SuperCallRewritingTest.java
new file mode 100644
index 0000000..7a3d45c
--- /dev/null
+++ b/src/test/examples/classmerging/SuperCallRewritingTest.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class SuperCallRewritingTest {
+  public static void main(String[] args) {
+    System.out.println("Calling referencedMethod on SubClassThatReferencesSuperMethod");
+    System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
+  }
+}
diff --git a/src/test/examples/classmerging/SuperClassWithReferencedMethod.java b/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
index 8d4e7b5..3bd5886c84 100644
--- a/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
+++ b/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
@@ -6,6 +6,7 @@
 public class SuperClassWithReferencedMethod {
 
   public String referencedMethod() {
-    return "From Super";
+    System.out.println("In referencedMethod on SuperClassWithReferencedMethod");
+    return "SuperClassWithReferencedMethod.referencedMethod()";
   }
 }
diff --git a/src/test/examples/classmerging/TemplateMethodTest.java b/src/test/examples/classmerging/TemplateMethodTest.java
new file mode 100644
index 0000000..ac9de15
--- /dev/null
+++ b/src/test/examples/classmerging/TemplateMethodTest.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2018, 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 classmerging;
+
+public class TemplateMethodTest {
+
+  public static void main(String[] args) {
+    AbstractClass obj = new AbstractClassImpl();
+    obj.foo();
+  }
+
+  private abstract static class AbstractClass {
+
+    public void foo() {
+      System.out.println("In foo on AbstractClass");
+      bar();
+    }
+
+    protected abstract void bar();
+  }
+
+  public static final class AbstractClassImpl extends AbstractClass {
+
+    @Override
+    public void bar() {
+      System.out.println("In bar on AbstractClassImpl");
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 1cc5b87..01e35f3 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,21 @@
 -keep public class classmerging.Test {
   public static void main(...);
 }
+-keep public class classmerging.ConflictingInterfaceSignaturesTest {
+  public static void main(...);
+}
+-keep public class classmerging.ExceptionTest {
+  public static void main(...);
+}
+-keep public class classmerging.SimpleInterfaceAccessTest {
+  public static void main(...);
+}
+-keep public class classmerging.SuperCallRewritingTest {
+  public static void main(...);
+}
+-keep public class classmerging.TemplateMethodTest {
+  public static void main(...);
+}
 
 # TODO(herhut): Consider supporting merging of inner-class attributes.
 # -keepattributes *
\ No newline at end of file
diff --git a/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java b/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java
new file mode 100644
index 0000000..5cfa11e
--- /dev/null
+++ b/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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 classmerging.pkg;
+
+import classmerging.SimpleInterface;
+
+public class SimpleInterfaceImplRetriever {
+
+  public static SimpleInterface getSimpleInterfaceImpl() {
+    return new SimpleInterfaceImpl();
+  }
+
+  // This class is intentionally marked private. It is not possible to merge the interface
+  // SimpleInterface into SimpleInterfaceImpl, since this would lead to an illegal class access
+  // in SimpleInterfaceAccessTest.
+  private static class SimpleInterfaceImpl implements SimpleInterface {
+
+    @Override
+    public void foo() {
+      System.out.println("In foo on SimpleInterfaceImpl");
+    }
+  }
+}
diff --git a/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
new file mode 100644
index 0000000..781d2b4
--- /dev/null
+++ b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2018 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 uninitializedfinal;
+
+// Test that leaks an instance before its final field has been initialized to a thread that
+// reads that field. This tests that redundant field load elimination does not eliminate
+// field reads (even of final fields) that cross a monitor operation.
+public class UninitializedFinalFieldLeak {
+
+  public static class PollingThread extends Thread {
+    public int result = 0;
+    UninitializedFinalFieldLeak f;
+
+    PollingThread(UninitializedFinalFieldLeak f) {
+      this.f = f;
+    }
+
+    // Read the field a number of times. Then lock on the object to await field initialization.
+    public void run() {
+      result += f.i;
+      result += f.i;
+      result += f.i;
+      f.threadReadsDone = true;
+      synchronized (f) {
+        result += f.i;
+      }
+      // The right result is 42. Reading the uninitialized 0 three times and then
+      // reading the initialized value. It is safe to remove the two redundant loads
+      // before the monitor operation.
+      System.out.println(result);
+    }
+  }
+
+  public final int i;
+  public volatile boolean threadReadsDone = false;
+
+  public UninitializedFinalFieldLeak() throws InterruptedException {
+    // Leak the object to a thread and start the thread with the lock on the object taken.
+    // Then allow the other thread to run and read the uninitialized field.
+    // Finally, initialize the field and release the lock.
+    PollingThread t = new PollingThread(this);
+    synchronized (this) {
+      t.start();
+      while (!threadReadsDone) {
+        Thread.yield();
+      }
+      i = 42;
+    }
+    t.join();
+  }
+
+  public static void main(String[] args) throws InterruptedException {
+    new UninitializedFinalFieldLeak();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
new file mode 100644
index 0000000..6ceb0a2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
@@ -0,0 +1,43 @@
+// Copyright (c) 2018 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;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ArchiveClassFileProviderTest {
+
+  @Rule
+  public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  public Path createZip() throws IOException {
+    Path tempRoot = temporaryFolder.getRoot().toPath();
+    Path zipFile = tempRoot.resolve("zipfile.zip");
+    ZipOutputStream zipStream =
+        new ZipOutputStream(new FileOutputStream(zipFile.toFile()), StandardCharsets.UTF_8);
+    ZipEntry entry = new ZipEntry("non-ascii:$\u02CF");
+    zipStream.putNextEntry(entry);
+    zipStream.write(10);
+    zipStream.close();
+    return zipFile;
+  }
+
+  @Test
+  public void testSystemLocale() throws IOException, ResourceException {
+    // Set the locale used for Paths to ASCII which will make Path creation fail
+    // for non-ascii names.
+    System.setProperty("sun.jnu.encoding", "ASCII");
+    Path zipFile = createZip();
+    new ArchiveClassFileProvider(zipFile);
+    ArchiveProgramResourceProvider.fromArchive(zipFile).getProgramResources();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 9cc2b48..7a44516 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -23,7 +23,7 @@
 
   protected void ensureException(String main, Class<? extends Throwable> exceptionClass,
       byte[]... classes) throws Exception {
-    ensureExceptionThrown(runOnJava(main, classes), exceptionClass);
+    ensureExceptionThrown(runOnJavaRaw(main, classes), exceptionClass);
     AndroidApp app = buildAndroidApp(classes);
     ensureExceptionThrown(runOnArtRaw(compileWithD8(app), main), exceptionClass);
     ensureExceptionThrown(runOnArtRaw(compileWithR8(app), main), exceptionClass);
@@ -37,7 +37,7 @@
       throws Exception {
     AndroidApp app = buildAndroidApp(classes);
     Consumer<InternalOptions> setMinApiLevel = o -> o.minApiLevel = apiLevel.getLevel();
-    ProcessResult javaResult = runOnJava(main, classes);
+    ProcessResult javaResult = runOnJavaRaw(main, classes);
     ProcessResult d8Result = runOnArtRaw(compileWithD8(app, setMinApiLevel), main);
     ProcessResult r8Result = runOnArtRaw(compileWithR8(app, setMinApiLevel), main);
     ProcessResult r8ShakenResult = runOnArtRaw(
@@ -56,7 +56,7 @@
   private void ensureSameOutput(String main, AndroidApp app, byte[]... classes)
       throws IOException, ExecutionException, CompilationFailedException,
           ProguardRuleParserException {
-    ProcessResult javaResult = runOnJava(main, classes);
+    ProcessResult javaResult = runOnJavaRaw(main, classes);
     ProcessResult d8Result = runOnArtRaw(compileWithD8(app), main);
     ProcessResult r8Result = runOnArtRaw(compileWithR8(app), main);
     ProcessResult r8ShakenResult = runOnArtRaw(
@@ -64,6 +64,10 @@
     Assert.assertEquals(javaResult.stdout, d8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8ShakenResult.stdout);
+    Assert.assertEquals(0, javaResult.exitCode);
+    Assert.assertEquals(0, d8Result.exitCode);
+    Assert.assertEquals(0, r8Result.exitCode);
+    Assert.assertEquals(0, r8ShakenResult.exitCode);
   }
 
   protected void ensureR8FailsWithCompilationError(String main, byte[]... classes)
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index 4e07849..4f749b5 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -85,6 +85,11 @@
   }
 
   @Test
+  public void testInlining() throws Exception {
+    runTest("inlining.Inlining");
+  }
+
+  @Test
   public void testInstanceVariable() throws Exception {
     runTest("instancevariable.InstanceVariable");
   }
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index 1b846a5..87453d0 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -6,13 +6,13 @@
 import static com.android.tools.r8.R8CommandTest.getOutputPath;
 import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static java.nio.file.StandardOpenOption.CREATE_NEW;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.sdklib.AndroidVersion;
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
@@ -23,8 +23,8 @@
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -263,13 +263,37 @@
         tmpClassesDir.toString());
     AndroidApp inputApp = ToolHelper.getApp(command);
     assertEquals(1, inputApp.getClasspathResourceProviders().size());
+    OrderedClassFileResourceProvider classpathProvider =
+        (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+    assertEquals(1, classpathProvider.providers.size());
     assertTrue(Files.isSameFile(tmpClassesDir,
-        ((DirectoryClassFileProvider) inputApp.getClasspathResourceProviders().get(0)).getRoot()));
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
     assertEquals(1, inputApp.getLibraryResourceProviders().size());
     assertTrue(Files.isSameFile(tmpClassesDir,
         ((DirectoryClassFileProvider) inputApp.getLibraryResourceProviders().get(0)).getRoot()));
   }
 
+  @Test
+  public void folderClasspathMultiple() throws Throwable {
+    Path inputFile =
+        Paths.get(ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR, "interfacemethods" + JAR_EXTENSION);
+    Path tmpClassesDir1 = temp.newFolder().toPath();
+    Path tmpClassesDir2 = temp.newFolder().toPath();
+    ZipUtils.unzip(inputFile.toString(), tmpClassesDir1.toFile());
+    ZipUtils.unzip(inputFile.toString(), tmpClassesDir2.toFile());
+    D8Command command = parse("--classpath", tmpClassesDir1.toString(), "--classpath",
+        tmpClassesDir2.toString());
+    AndroidApp inputApp = ToolHelper.getApp(command);
+    assertEquals(1, inputApp.getClasspathResourceProviders().size());
+    OrderedClassFileResourceProvider classpathProvider =
+        (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+    assertEquals(2, classpathProvider.providers.size());
+    assertTrue(Files.isSameFile(tmpClassesDir1,
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
+    assertTrue(Files.isSameFile(tmpClassesDir2,
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(1)).getRoot()));
+  }
+
   @Test(expected = CompilationFailedException.class)
   public void classFolderProgram() throws Throwable {
     Path inputFile =
@@ -319,7 +343,7 @@
     Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
     ProgramResourceProvider myProvider =
         ArchiveProgramResourceProvider.fromSupplier(
-            new MyOrigin(), () -> new ZipFile(input.toFile()));
+            new MyOrigin(), () -> new ZipFile(input.toFile(), StandardCharsets.UTF_8));
     D8Command command =
         D8Command.builder()
             .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
@@ -464,7 +488,7 @@
             .setOutput(emptyZip, OutputMode.DexIndexed)
             .build());
     assertTrue(Files.exists(emptyZip));
-    assertEquals(0, new ZipFile(emptyZip.toFile()).size());
+    assertEquals(0, new ZipFile(emptyZip.toFile(), StandardCharsets.UTF_8).size());
   }
 
   private D8Command parse(String... args) throws CompilationFailedException {
diff --git a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
index 258aa43..ee82fc4 100644
--- a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
+++ b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
@@ -1920,11 +1920,6 @@
           // 1) t03
           // java.lang.AssertionError: expected null, but was:<[I@1b32f32>
 
-          .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01",
-              match(runtimes(Version.DEFAULT, Version.V7_0_0, Version.V6_0_1, Version.V5_1_1)))
-          // 1) t03
-          // java.lang.AssertionError: reference is not enqueued after 2 sec
-
           .put("lang.StackTraceElement.toString.StackTraceElement_toString_A01",
               match(runtimes(Version.DEFAULT)))
           // 1) t03
@@ -4959,8 +4954,9 @@
           .put("lang.ref.PhantomReference.isEnqueued.PhantomReference_isEnqueued_A01",
               match(runtimesUpTo(Version.V4_4_4)))
 
-          .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01",
-              match(runtimesUpTo(Version.V4_4_4)))
+          .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01", any())
+          // 1) t03
+          // java.lang.AssertionError: reference is not enqueued after 2 sec
 
           .put("lang.ref.WeakReference.enqueue.WeakReference_enqueue_A01",
               match(runtimesUpTo(Version.V4_4_4)))
diff --git a/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
new file mode 100644
index 0000000..dc497c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2018, 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+
+public class OrderedClassFileResourceProviderTest extends TestBase {
+  class SimpleClassFileResourceProvider implements ClassFileResourceProvider {
+
+    private final Set<String> descriptors;
+    private final ProgramResource fixedProgramResource;
+
+    SimpleClassFileResourceProvider(int id, Set<String> descriptors) {
+      this.descriptors = descriptors;
+      this.fixedProgramResource = new SimpleProgramResource(id);
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return descriptors;
+    }
+
+    @Override
+    public ProgramResource getProgramResource(String descriptor) {
+      return fixedProgramResource;
+    }
+  }
+
+  class SimpleProgramResource implements ProgramResource {
+
+    private final Origin origin;
+
+    SimpleProgramResource(int id) {
+      origin = new SimpleOrigin(id);
+    }
+
+    @Override
+    public Kind getKind() {
+      return null;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      return null;
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return null;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+  }
+
+  public class SimpleOrigin extends Origin {
+
+    private final int id;
+
+    private SimpleOrigin(int index) {
+      super(root());
+      this.id = index;
+    }
+
+    int getId() {
+      return id;
+    }
+
+    @Override
+    public String part() {
+      return "Test";
+    }
+  }
+
+  @Test
+  public void test() {
+    OrderedClassFileResourceProvider.Builder builder = OrderedClassFileResourceProvider.builder();
+    builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(1, ImmutableSet.of(
+        "L/a/a/a", "L/a/a/b", "L/a/a/c"
+    )));
+    builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(2, ImmutableSet.of(
+        "L/a/a/b", "L/a/a/c", "L/a/a/d"
+        )));
+    ClassFileResourceProvider provider = builder.build();
+    assertEquals(
+        ImmutableSet.of("L/a/a/a", "L/a/a/b", "L/a/a/c", "L/a/a/d"),
+        provider.getClassDescriptors());
+
+    Map<String, Integer> expectations = ImmutableMap.of(
+        "L/a/a/a", 1,
+        "L/a/a/b", 1,
+        "L/a/a/c", 1,
+        "L/a/a/d", 2
+    );
+    expectations.forEach((descriptor, id) ->
+        assertEquals(
+            (int) id,
+            ((SimpleOrigin) provider.getProgramResource(descriptor).getOrigin()).getId()));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index fbd88be..16bbae9 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -459,7 +460,7 @@
             .setOutput(emptyZip, OutputMode.DexIndexed)
             .build());
     assertTrue(Files.exists(emptyZip));
-    assertEquals(0, new ZipFile(emptyZip.toFile()).size());
+    assertEquals(0, new ZipFile(emptyZip.toFile(), StandardCharsets.UTF_8).size());
   }
 
   private R8Command parse(String... args) throws CompilationFailedException {
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index fe5ae72..3d3b4e7 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -145,7 +145,7 @@
 
   @Before
   public void compile() throws Exception {
-    if (output == Output.CF && getFailingCompileCf().contains(mainClass)) {
+    if (shouldCompileFail()) {
       thrown.expect(Throwable.class);
     }
     OutputMode outputMode = output == Output.CF ? OutputMode.ClassFile : OutputMode.DexIndexed;
@@ -186,8 +186,25 @@
     }
   }
 
+  private boolean shouldCompileFail() {
+    if (output == Output.CF && getFailingCompileCf().contains(mainClass)) {
+      return true;
+    }
+    if (frontend == Frontend.CF
+        && output == Output.DEX
+        && getFailingCompileCfToDex().contains(mainClass)) {
+      return true;
+    }
+    return false;
+  }
+
   @Test
   public void outputIsIdentical() throws IOException, InterruptedException, ExecutionException {
+    if (shouldCompileFail()) {
+      // We expected an exception, but got none.
+      // Return to ensure that this test fails due to the missing exception.
+      return;
+    }
     Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
 
@@ -211,8 +228,12 @@
         output == Output.CF ? getFailingRunCf().get(mainClass) : getFailingRun().get(mainClass);
     if (condition != null && condition.test(getTool(), compiler, vm.getVersion(), mode)) {
       thrown.expect(Throwable.class);
-    } else {
-      thrown = ExpectedException.none();
+    }
+
+    if (frontend == Frontend.CF
+        && output == Output.DEX
+        && getFailingRunCfToDex().contains(mainClass)) {
+      thrown.expect(Throwable.class);
     }
 
     if (output == Output.CF) {
@@ -257,6 +278,11 @@
 
   protected abstract Map<String, TestCondition> getFailingRunCf();
 
+  protected abstract Set<String> getFailingCompileCfToDex();
+
+  // TODO(mathiasr): Add CompilerSet for CfToDex so we can fold this into getFailingRun().
+  protected abstract Set<String> getFailingRunCfToDex();
+
   protected abstract Set<String> getFailingCompileCf();
 
   protected abstract Set<String> getFailingOutputCf();
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
index 97c8624..dc94a71 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
@@ -49,6 +49,16 @@
   }
 
   @Override
+  protected Set<String> getFailingCompileCfToDex() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  protected Set<String> getFailingRunCfToDex() {
+    return Collections.emptySet();
+  }
+
+  @Override
   protected Set<String> getFailingCompileCf() {
     return Collections.emptySet();
   }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index d8b886c..36d924f 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -41,12 +41,15 @@
         "filledarray.FilledArray",
         "hello.Hello",
         "ifstatements.IfStatements",
+        "inlining.Inlining",
         "instancevariable.InstanceVariable",
         "instanceofstring.InstanceofString",
         "invoke.Invoke",
+        "invokeempty.InvokeEmpty",
         "jumbostring.JumboString",
         "loadconst.LoadConst",
         "loop.UdpServer",
+        "nestedtrycatches.NestedTryCatches",
         "newarray.NewArray",
         "regalloc.RegAlloc",
         "returns.Returns",
@@ -57,9 +60,7 @@
         "throwing.Throwing",
         "trivial.Trivial",
         "trycatch.TryCatch",
-        "nestedtrycatches.NestedTryCatches",
         "trycatchmany.TryCatchMany",
-        "invokeempty.InvokeEmpty",
         "regress.Regress",
         "regress2.Regress2",
         "regress_37726195.Regress",
@@ -80,6 +81,7 @@
         "enclosingmethod_proguarded.Main",
         "interfaceinlining.Main",
         "switchmaps.Switches",
+        "uninitializedfinal.UninitializedFinalFieldLeak",
     };
 
     List<String[]> fullTestList = new ArrayList<>(tests.length * 2);
@@ -109,6 +111,14 @@
               test,
               Frontend.CF,
               Output.CF));
+      fullTestList.add(
+          makeTest(
+              Input.JAVAC_ALL,
+              CompilerUnderTest.R8,
+              CompilationMode.RELEASE,
+              test,
+              Frontend.CF,
+              Output.DEX));
     }
     return fullTestList;
   }
@@ -143,6 +153,29 @@
   }
 
   @Override
+  protected Set<String> getFailingRunCfToDex() {
+    return new ImmutableSet.Builder<String>()
+        // TODO(b/109788783): Implement byte/boolean distinction for array load/store.
+        .add("arrayaccess.ArrayAccess")
+        .add("barray.BArray")
+        .add("filledarray.FilledArray")
+        .build();
+  }
+
+  @Override
+  protected Set<String> getFailingCompileCfToDex() {
+    return new ImmutableSet.Builder<String>()
+        // TODO(b/109789541): Implement method synchronization for DEX backend.
+        .add("sync.Sync")
+        // TODO(b/109789539): Implement CfMultiANewArray.buildIR() for DEX backend.
+        .add("newarray.NewArray")
+        .add("trycatch.TryCatch")
+        .add("regress_70737019.Test")
+        .add("regress_72361252.Test")
+        .build();
+  }
+
+  @Override
   protected Set<String> getFailingCompileCf() {
     return new ImmutableSet.Builder<String>()
         .build();
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 2c6a72a..b0876c9 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -27,6 +27,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -580,7 +581,7 @@
 
   protected DexInspector getMainDexInspector(Path zip)
       throws ZipException, IOException, ExecutionException {
-    try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+    try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
       try (InputStream in =
           zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
         return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index 182fe80..4701ac3 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -25,6 +25,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -256,7 +257,7 @@
 
   protected DexInspector getMainDexInspector(Path zip)
       throws ZipException, IOException, ExecutionException {
-    try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+    try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
       try (InputStream in =
           zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
         return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 284ed68..9560c1f 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -25,6 +25,7 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -309,7 +310,7 @@
 
   protected DexInspector getMainDexInspector(Path zip)
       throws ZipException, IOException, ExecutionException {
-    try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+    try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
       try (InputStream in =
           zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
         return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index add86c2..5200a09 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -151,6 +151,11 @@
     return builder.build();
   }
 
+  /** Build an AndroidApp from the specified program files. */
+  protected AndroidApp readProgramFiles(Path... programFiles) throws IOException {
+    return AndroidApp.builder().addProgramFiles(programFiles).build();
+  }
+
   /**
    * Create a temporary JAR file containing the specified test classes.
    */
@@ -503,7 +508,7 @@
     return result.stdout;
   }
 
-  protected ProcessResult runOnJava(String main, byte[]... classes) throws IOException {
+  protected ProcessResult runOnJavaRaw(String main, byte[]... classes) throws IOException {
     Path file = writeToZip(Arrays.asList(classes));
     return ToolHelper.runJavaNoVerify(file, main);
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 000076e..d5d0558 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -807,7 +807,9 @@
 
   public static R8Command.Builder prepareR8CommandBuilder(
       AndroidApp app, ProgramConsumer programConsumer) {
-    return R8Command.builder(app).setProgramConsumer(programConsumer);
+    return R8Command.builder(app)
+        .setProgramConsumer(programConsumer)
+        .setProguardMapConsumer(StringConsumer.emptyConsumer());
   }
 
   public static AndroidApp runR8(AndroidApp app) throws IOException {
@@ -1413,6 +1415,11 @@
     return result.stdout;
   }
 
+  public static ProcessResult runProguardRaw(
+      Path inJar, Path outJar, Path lib, Path config, Path map) throws IOException {
+    return runProguardRaw(getProguardScript(), inJar, outJar, lib, ImmutableList.of(config), map);
+  }
+
   public static ProcessResult runProguardRaw(Path inJar, Path outJar, List<Path> config, Path map)
       throws IOException {
     return runProguardRaw(getProguardScript(), inJar, outJar, config, map);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java
new file mode 100644
index 0000000..160ad59
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2018, 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.bridgeremoval;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class EmptyBridgeTest extends TestBase {
+
+  @Test
+  public void test() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder abs = jasminBuilder.addClass("Abs");
+    abs.setAccess("public abstract");
+    abs.addMethod("public bridge abstract", "emptyBridge", ImmutableList.of(), "V");
+
+    ClassBuilder cls = jasminBuilder.addClass("Main");
+    cls.addVirtualMethod("bogus", ImmutableList.of(), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "ldc \"foo\"",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
+        "return");
+    cls.addMainMethod(
+        ".limit stack 4",
+        ".limit locals 2",
+        "new " + cls.name,
+        "dup",
+        "invokespecial " + cls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "invokevirtual " + cls.name + "/bogus()V",
+        "return");
+
+    String absClassName = abs.name;
+    String mainClassName = cls.name;
+    String proguardConfig =
+        "-allowaccessmodification" + System.lineSeparator()
+        + "-keep class " + mainClassName + "{ *; }" + System.lineSeparator()
+            + "-keep class " + absClassName + "{ *; }";
+    AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig);
+
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject classSubject = inspector.clazz(absClassName);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method("void", "emptyBridge", ImmutableList.of());
+    assertThat(methodSubject, isPresent());
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index 6069b4c..da32cce 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -31,7 +31,6 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
-
 @RunWith(Parameterized.class)
 public class MethodHandleTestRunner extends TestBase {
   static final Class<?> CLASS = MethodHandleTest.class;
@@ -169,7 +168,9 @@
           Arrays.asList(
               "-keep public class com.android.tools.r8.cf.MethodHandleTest {",
               "  public static void main(...);",
-              "}"),
+              "}",
+              // Disallow merging MethodHandleTest$I into MethodHandleTest$Impl
+              "-keep public interface com.android.tools.r8.cf.MethodHandleTest$I"),
           Origin.unknown());
     }
     try {
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index d5d2c39..7016163 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,29 +3,48 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.MoveException;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
-import org.junit.Rule;
+import org.junit.Ignore;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 
-public class ClassMergingTest {
+// TODO(christofferqa): Add tests to check that statically typed invocations on method handles
+// continue to work after class merging. Rewriting of method handles should be carried out by
+// LensCodeRewriter.rewriteDexMethodHandle.
+public class ClassMergingTest extends TestBase {
 
+  private static final Path CF_DIR =
+      Paths.get(ToolHelper.BUILD_DIR).resolve("classes/examples/classmerging");
   private static final Path EXAMPLE_JAR = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR)
       .resolve("classmerging.jar");
   private static final Path EXAMPLE_KEEP = Paths.get(ToolHelper.EXAMPLES_DIR)
@@ -33,17 +52,14 @@
   private static final Path DONT_OPTIMIZE = Paths.get(ToolHelper.EXAMPLES_DIR)
       .resolve("classmerging").resolve("keep-rules-dontoptimize.txt");
 
-  @Rule
-  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
   private void configure(InternalOptions options) {
     options.enableClassMerging = true;
     options.enableClassInlining = false;
+    options.enableMinification = false;
   }
 
   private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
-      throws IOException, ProguardRuleParserException, ExecutionException,
-      CompilationFailedException {
+      throws IOException, ExecutionException, CompilationFailedException {
     ToolHelper.runR8(
         R8Command.builder()
             .setOutput(Paths.get(temp.getRoot().getCanonicalPath()), OutputMode.DexIndexed)
@@ -73,13 +89,12 @@
       assertFalse(inspector.clazz(candidate).isPresent());
     }
     assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
-    assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
     assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent());
     assertTrue(inspector.clazz("classmerging.SubClass").isPresent());
   }
 
   @Test
-  public void testClassesShouldNotMerged() throws Exception {
+  public void testClassesHaveNotBeenMerged() throws Exception {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
       assertTrue(inspector.clazz(candidate).isPresent());
@@ -95,9 +110,243 @@
 
   @Test
   public void testSuperCallWasDetected() throws Exception {
-    runR8(EXAMPLE_KEEP, this::configure);
-    assertTrue(inspector.clazz("classmerging.SuperClassWithReferencedMethod").isPresent());
-    assertTrue(inspector.clazz("classmerging.SubClassThatReferencesSuperMethod").isPresent());
+    String main = "classmerging.SuperCallRewritingTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("SubClassThatReferencesSuperMethod.class"),
+          CF_DIR.resolve("SuperClassWithReferencedMethod.class"),
+          CF_DIR.resolve("SuperCallRewritingTest.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SubClassThatReferencesSuperMethod",
+            "classmerging.SuperCallRewritingTest");
+    runTest(main, programFiles, preservedClassNames);
   }
 
+  // When a subclass A has been merged into its subclass B, we rewrite invoke-super calls that hit
+  // methods in A to invoke-direct calls. However, we should be careful not to transform invoke-
+  // super instructions into invoke-direct instructions simply because the static target is a method
+  // in the enclosing class.
+  //
+  // This test hand-crafts an invoke-super instruction in SubClassThatReferencesSuperMethod that
+  // targets SubClassThatReferencesSuperMethod.referencedMethod. When running without class
+  // merging, R8 should not rewrite the invoke-super instruction into invoke-direct.
+  @Test
+  public void testSuperCallNotRewrittenToDirect() throws Exception {
+    String main = "classmerging.SuperCallRewritingTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("SuperClassWithReferencedMethod.class"),
+          CF_DIR.resolve("SuperCallRewritingTest.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SubClassThatReferencesSuperMethod",
+            "classmerging.SuperClassWithReferencedMethod",
+            "classmerging.SuperCallRewritingTest");
+
+    // Build SubClassThatReferencesMethod.
+    SmaliBuilder smaliBuilder =
+        new SmaliBuilder(
+            "classmerging.SubClassThatReferencesSuperMethod",
+            "classmerging.SuperClassWithReferencedMethod");
+    smaliBuilder.addInitializer(
+        ImmutableList.of(),
+        0,
+        "invoke-direct {p0}, Lclassmerging/SuperClassWithReferencedMethod;-><init>()V",
+        "return-void");
+    smaliBuilder.addInstanceMethod(
+        "java.lang.String",
+        "referencedMethod",
+        ImmutableList.of(),
+        2,
+        "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "const-string v1, \"In referencedMethod on SubClassThatReferencesSuperMethod\"",
+        "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+        "invoke-super {p0}, Lclassmerging/SubClassThatReferencesSuperMethod;->referencedMethod()Ljava/lang/String;",
+        "move-result-object v1",
+        "return-object v1");
+
+    // Build app.
+    AndroidApp.Builder builder = AndroidApp.builder();
+    builder.addProgramFiles(programFiles);
+    builder.addDexProgramData(smaliBuilder.compile(), Origin.unknown());
+
+    // Run test.
+    runTestOnInput(
+        main,
+        builder.build(),
+        preservedClassNames,
+        // Prevent class merging, such that the generated code would be invalid if we rewrite the
+        // invoke-super instruction into an invoke-direct instruction.
+        getProguardConfig(EXAMPLE_KEEP, "-keep class *"));
+  }
+
+  @Test
+  public void testConflictingInterfaceSignatures() throws Exception {
+    String main = "classmerging.ConflictingInterfaceSignaturesTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ConflictingInterfaceSignaturesTest.class"),
+          CF_DIR.resolve("ConflictingInterfaceSignaturesTest$A.class"),
+          CF_DIR.resolve("ConflictingInterfaceSignaturesTest$B.class"),
+          CF_DIR.resolve("ConflictingInterfaceSignaturesTest$InterfaceImpl.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ConflictingInterfaceSignaturesTest",
+            "classmerging.ConflictingInterfaceSignaturesTest$InterfaceImpl");
+    runTest(main, programFiles, preservedClassNames);
+  }
+
+  // If an exception class A is merged into another exception class B, then all exception tables
+  // should be updated, and class A should be removed entirely.
+  @Test
+  public void testExceptionTables() throws Exception {
+    String main = "classmerging.ExceptionTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ExceptionTest.class"),
+          CF_DIR.resolve("ExceptionTest$ExceptionA.class"),
+          CF_DIR.resolve("ExceptionTest$ExceptionB.class"),
+          CF_DIR.resolve("ExceptionTest$Exception1.class"),
+          CF_DIR.resolve("ExceptionTest$Exception2.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ExceptionTest",
+            "classmerging.ExceptionTest$ExceptionB",
+            "classmerging.ExceptionTest$Exception2");
+    DexInspector inspector = runTest(main, programFiles, preservedClassNames);
+
+    ClassSubject mainClass = inspector.clazz(main);
+    assertThat(mainClass, isPresent());
+
+    MethodSubject mainMethod =
+        mainClass.method("void", "main", ImmutableList.of("java.lang.String[]"));
+    assertThat(mainMethod, isPresent());
+
+    // Check that the second catch handler has been removed.
+    DexCode code = mainMethod.getMethod().getCode().asDexCode();
+    int numberOfMoveExceptionInstructions = 0;
+    for (Instruction instruction : code.instructions) {
+      if (instruction instanceof MoveException) {
+        numberOfMoveExceptionInstructions++;
+      }
+    }
+    assertEquals(2, numberOfMoveExceptionInstructions);
+  }
+
+  @Test
+  public void testNoIllegalClassAccess() throws Exception {
+    String main = "classmerging.SimpleInterfaceAccessTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("SimpleInterface.class"),
+          CF_DIR.resolve("SimpleInterfaceAccessTest.class"),
+          CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
+          CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class")
+        };
+    // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl
+    // is in a different package and is not public.
+    ImmutableSet<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SimpleInterface",
+            "classmerging.SimpleInterfaceAccessTest",
+            "classmerging.pkg.SimpleInterfaceImplRetriever",
+            "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+    runTest(main, programFiles, preservedClassNames);
+  }
+
+  @Ignore("b/73958515")
+  @Test
+  public void testNoIllegalClassAccessWithAccessModifications() throws Exception {
+    // If access modifications are allowed then SimpleInterface should be merged into
+    // SimpleInterfaceImpl.
+    String main = "classmerging.SimpleInterfaceAccessTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("SimpleInterface.class"),
+          CF_DIR.resolve("SimpleInterfaceAccessTest.class"),
+          CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
+          CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class")
+        };
+    ImmutableSet<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SimpleInterfaceAccessTest",
+            "classmerging.pkg.SimpleInterfaceImplRetriever",
+            "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+    // Allow access modifications (and prevent SimpleInterfaceImplRetriever from being removed as
+    // a result of inlining).
+    runTest(
+        main,
+        programFiles,
+        preservedClassNames,
+        getProguardConfig(
+            EXAMPLE_KEEP,
+            "-allowaccessmodification",
+            "-keep public class classmerging.pkg.SimpleInterfaceImplRetriever"));
+  }
+
+  @Test
+  public void testTemplateMethodPattern() throws Exception {
+    String main = "classmerging.TemplateMethodTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("TemplateMethodTest.class"),
+          CF_DIR.resolve("TemplateMethodTest$AbstractClass.class"),
+          CF_DIR.resolve("TemplateMethodTest$AbstractClassImpl.class")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.TemplateMethodTest", "classmerging.TemplateMethodTest$AbstractClassImpl");
+    runTest(main, programFiles, preservedClassNames);
+  }
+
+  private DexInspector runTest(String main, Path[] programFiles, Set<String> preservedClassNames)
+      throws Exception {
+    return runTest(main, programFiles, preservedClassNames, getProguardConfig(EXAMPLE_KEEP));
+  }
+
+  private DexInspector runTest(
+      String main, Path[] programFiles, Set<String> preservedClassNames, String proguardConfig)
+      throws Exception {
+    return runTestOnInput(
+        main, readProgramFiles(programFiles), preservedClassNames, proguardConfig);
+  }
+
+  private DexInspector runTestOnInput(
+      String main, AndroidApp input, Set<String> preservedClassNames, String proguardConfig)
+      throws Exception {
+    AndroidApp output = compileWithR8(input, proguardConfig, this::configure);
+    DexInspector inspector = new DexInspector(output);
+    // Check that all classes in [preservedClassNames] are in fact preserved.
+    for (String className : preservedClassNames) {
+      assertTrue(
+          "Class " + className + " should be present", inspector.clazz(className).isPresent());
+    }
+    // Check that all other classes have been removed.
+    for (FoundClassSubject classSubject : inspector.allClasses()) {
+      String className = classSubject.getDexClass().toSourceString();
+      assertTrue(
+          "Class " + className + " should be absent", preservedClassNames.contains(className));
+    }
+    // Check that the R8-generated code produces the same result as D8-generated code.
+    assertEquals(runOnArt(compileWithD8(input), main), runOnArt(output, main));
+    return inspector;
+  }
+
+  private String getProguardConfig(Path path, String... additionalRules) throws IOException {
+    StringBuilder builder = new StringBuilder();
+    for (String line : Files.readAllLines(path)) {
+      builder.append(line);
+      builder.append(System.lineSeparator());
+    }
+    for (String rule : additionalRules) {
+      builder.append(rule);
+      builder.append(System.lineSeparator());
+    }
+    return builder.toString();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java b/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
index e49c740..341fb67 100644
--- a/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
+++ b/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.Enumeration;
 import java.util.HashSet;
@@ -65,7 +66,7 @@
     for (String className : CLASS_NAMES) {
       expectedNames.add(SUBDIR + "/" + className + ".class.dex");
     }
-    try (ZipFile zipFile = new ZipFile(outputZip.toFile())) {
+    try (ZipFile zipFile = new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8)) {
       for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
         ZipEntry ze = e.nextElement();
         expectedNames.remove(ze.getName());
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index c55428f..7d181bc 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
@@ -197,8 +198,8 @@
   }
 
   private void compareArchiveFiles(Path d8File, Path dxFile) throws IOException {
-    ZipFile d8Zip = new ZipFile(d8File.toFile());
-    ZipFile dxZip = new ZipFile(dxFile.toFile());
+    ZipFile d8Zip = new ZipFile(d8File.toFile(), StandardCharsets.UTF_8);
+    ZipFile dxZip = new ZipFile(dxFile.toFile(), StandardCharsets.UTF_8);
     // TODO(zerny): This should test resource containment too once supported.
     Set<String> d8Content = d8Zip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
     Set<String> dxContent = dxZip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
diff --git a/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java b/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java
new file mode 100644
index 0000000..2c1479e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokespecial.Main;
+import com.android.tools.r8.graph.invokespecial.TestClassDump;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class InvokeSpecialTest extends AsmTestBase {
+
+  @Ignore("b/110175213")
+  @Test
+  public void testInvokeSpecial() throws Exception {
+    ensureSameOutput(
+        Main.class.getCanonicalName(),
+        ToolHelper.getClassAsBytes(Main.class),
+        TestClassDump.dump());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java b/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java
new file mode 100644
index 0000000..deb316d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.invokespecial;
+
+public class Main {
+
+  public static void main(String[] args) {
+    TestClass x = new TestClass();
+    x.m(true);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java
new file mode 100644
index 0000000..c08091e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.invokespecial;
+
+public class TestClass {
+
+  public void m(boolean recurse) {
+    System.out.println(recurse);
+    if (recurse) {
+      m(false);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java
new file mode 100644
index 0000000..ae336834
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.invokespecial;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running ./tools/asmifier.py build/classes/test/com/android/tools/r8/graph/-
+// invokespecial/TestClass.class, and changing the invoke-virtual TestClass.m() instruction to
+// an invoke-special instruction.
+public class TestClassDump implements Opcodes {
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "com/android/tools/r8/graph/invokespecial/TestClass",
+        null,
+        "java/lang/Object",
+        null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "m", "(Z)V", null, null);
+      mv.visitCode();
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Z)V", false);
+      mv.visitVarInsn(ILOAD, 1);
+      Label l0 = new Label();
+      mv.visitJumpInsn(IFEQ, l0);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitInsn(ICONST_0);
+      // Note: Changed from INVOKEVIRTUAL to INVOKESPECIAL.
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "com/android/tools/r8/graph/invokespecial/TestClass", "m", "(Z)V", false);
+      mv.visitLabel(l0);
+      mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java b/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java
new file mode 100644
index 0000000..3f7bcfb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, 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;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import org.junit.Test;
+
+public class B87341268 extends TestBase {
+  @Test
+  public void test() throws Exception {
+    AndroidApp app = compileWithD8(readClasses(TestClass.class));
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(TestClass.class);
+    assertThat(clazz, isPresent());
+  }
+}
+
+class TestClass {
+  int loop(String arg) {
+    long[] array = { 0L, 1L, 2L };
+    int length = -1;
+    while (true) {
+      try {
+        length = arg.length();
+      } catch (Exception e) {
+        System.err.println(e.getMessage());
+        break;
+      }
+    }
+    return length;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index d36e6ad..5df1cee 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -63,7 +63,7 @@
         ToolHelper.getClassAsBytes(ClassWithFinal.class)
     };
     String main = TrivialTestClass.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
 
     AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
@@ -135,7 +135,7 @@
         ToolHelper.getClassAsBytes(ControlFlow.class),
     };
     String main = BuildersTestClass.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
 
     AndroidApp app = runR8(buildAndroidApp(classes), BuildersTestClass.class);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
index 86c15a2..71dae86 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
@@ -60,7 +60,7 @@
         ToolHelper.getClassAsBytes(Main.class)
     };
     String main = Main.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
 
     AndroidApp originalApp = buildAndroidApp(classes);
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index eb6e943..ba04454 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -195,7 +195,7 @@
       return addStaticMethod("main", ImmutableList.of("[Ljava/lang/String;"), "V", lines);
     }
 
-    private MethodSignature addMethod(
+    public MethodSignature addMethod(
         String access,
         String name,
         List<String> argumentTypes,
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 74e3d5e..6ebb08b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -83,13 +83,16 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
-  private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+  private Consumer<InternalOptions> disableClassInliningAndMerging = o -> {
+    o.enableClassInlining = false;
+    o.enableClassMerging = false;
+  };
 
   @Test
   public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -112,7 +115,7 @@
   public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -134,7 +137,7 @@
   public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -157,7 +160,7 @@
   public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -180,7 +183,7 @@
   public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -203,7 +206,7 @@
   public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -228,7 +231,7 @@
   public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -252,7 +255,7 @@
   public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -275,7 +278,7 @@
   public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -296,7 +299,7 @@
   public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -315,7 +318,7 @@
   public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -334,7 +337,7 @@
   public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -351,7 +354,7 @@
   public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassIsKept(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -377,7 +380,7 @@
   public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector,
           "properties.CompanionProperties");
@@ -408,7 +411,7 @@
   public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector,
           "properties.CompanionProperties");
@@ -442,7 +445,7 @@
   public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector,
           "properties.CompanionProperties");
@@ -473,7 +476,7 @@
   public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector,
           "properties.CompanionProperties");
@@ -505,7 +508,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
@@ -536,7 +539,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
@@ -560,7 +563,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
diff --git a/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
new file mode 100644
index 0000000..957bc1d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
@@ -0,0 +1,474 @@
+// Copyright (c) 2018, 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.naming.signature.GenericSignatureAction;
+import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.lang.reflect.GenericSignatureFormatError;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.junit.Test;
+
+public class GenericSignatureParserTest extends TestBase {
+  private static class ReGenerateGenericSignatureRewriter
+      implements GenericSignatureAction<String> {
+
+    private StringBuilder renamedSignature;
+
+    public String getRenamedSignature() {
+      return renamedSignature.toString();
+    }
+
+    @Override
+    public void parsedSymbol(char symbol) {
+      renamedSignature.append(symbol);
+    }
+
+    @Override
+    public void parsedIdentifier(String identifier) {
+      renamedSignature.append(identifier);
+    }
+
+    @Override
+    public String parsedTypeName(String name) {
+      renamedSignature.append(name);
+      return name;
+    }
+
+    @Override
+    public String parsedInnerTypeName(String enclosingType, String name) {
+      renamedSignature.append(name);
+      return name;
+    }
+
+    @Override
+    public void start() {
+      renamedSignature = new StringBuilder();
+    }
+
+    @Override
+    public void stop() {
+      // nothing to do
+    }
+  }
+
+  public void parseSimpleError(BiConsumer<GenericSignatureParser<String>, String> parse,
+      Consumer<GenericSignatureFormatError> errorChecker) {
+    try {
+      String signature = "X";
+      GenericSignatureParser<String> parser =
+          new GenericSignatureParser<>(new ReGenerateGenericSignatureRewriter());
+      parse.accept(parser, signature);
+      fail("Succesfully parsed " + signature);
+    } catch (GenericSignatureFormatError e) {
+      errorChecker.accept(e);
+    }
+  }
+
+  @Test
+  public void simpleParseError() {
+    parseSimpleError(
+        GenericSignatureParser::parseClassSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected L at position 1")));
+    // TODO(sgjesse): The position 2 reported here is one off.
+    parseSimpleError(
+        GenericSignatureParser::parseFieldSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected L, [ or T at position 2")));
+    parseSimpleError(GenericSignatureParser::parseMethodSignature,
+        e -> assertTrue(e.getMessage().startsWith("Expected ( at position 1")));
+  }
+
+  private void parseSignature(String signature, Set<Integer> validPrefixes,
+      Consumer<String> parser, ReGenerateGenericSignatureRewriter rewriter) {
+    for (int i = 0; i < 2; i++) {
+      parser.accept(signature);
+      assertEquals(signature, rewriter.getRenamedSignature());
+    }
+
+    for (int i = 1; i < signature.length(); i++) {
+      try {
+        if (validPrefixes == null || !validPrefixes.contains(i)) {
+          parser.accept(signature.substring(0, i));
+          fail("Succesfully parsed " + signature.substring(0, i) + " (position " + i +")");
+        }
+      } catch (GenericSignatureFormatError e) {
+        assertTrue(e.getMessage().contains("at position " + (i + 1)));
+      }
+    }
+  }
+
+  private void parseClassSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseClassSignature, rewriter);
+  }
+
+  private void parseFieldSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseFieldSignature, rewriter);
+  }
+
+  private void parseMethodSignature(String signature, Set<Integer> validPrefixes) {
+    ReGenerateGenericSignatureRewriter rewriter = new ReGenerateGenericSignatureRewriter();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+
+    parseSignature(signature, validPrefixes, parser::parseMethodSignature, rewriter);
+  }
+
+  public void forClassSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    consumer.accept("Ljava/lang/Object;", null);
+    consumer.accept("LOuter$InnerInterface<TU;>;", null);
+
+    consumer.accept("La;", null);
+    consumer.accept("La.i;", null);
+    consumer.accept("La.i.j;", null);
+    consumer.accept("La/b;", null);
+    consumer.accept("La/b.i;", null);
+    consumer.accept("La/b.i.j;", null);
+    consumer.accept("La/b/c;", null);
+    consumer.accept("La/b/c.i;", null);
+    consumer.accept("La/b/c.i.j;", null);
+    consumer.accept("La$b;", null);
+    consumer.accept("La$b.i;", null);
+    consumer.accept("La$b.i.j;", null);
+    consumer.accept("La$b$c;", null);
+    consumer.accept("La$b$c.i;", null);
+    consumer.accept("La$b$c.i.j;", null);
+
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < 3; i++) {
+      builder.append("TT;");
+      consumer.accept("La<" + builder.toString() + ">;", null);
+      consumer.accept("La<" + builder.toString() + ">.i;", null);
+      consumer.accept("La<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La/b<" + builder.toString() + ">;", null);
+      consumer.accept("La/b<" + builder.toString() + ">.i;", null);
+      consumer.accept("La/b<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">.i;", null);
+      consumer.accept("La/b/c<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La$b<" + builder.toString() + ">;", null);
+      consumer.accept("La$b<" + builder.toString() + ">.i;", null);
+      consumer.accept("La$b<" + builder.toString() + ">.i.j;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">.i;", null);
+      consumer.accept("La$b$c<" + builder.toString() + ">.i.j;", null);
+    }
+  }
+
+
+  public void forBasicTypes(Consumer<String> consumer) {
+    for (char c : "BCDFIJSZ".toCharArray()) {
+      consumer.accept(new String(new char[]{c}));
+    }
+  }
+
+  public void forBasicTypesAndVoid(Consumer<String> consumer) {
+    forBasicTypes(consumer);
+    consumer.accept("V");
+  }
+
+  public void forTypeVariableSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    consumer.accept("TT;", null);
+
+    consumer.accept("Ta;", null);
+    consumer.accept("Tab;", null);
+    consumer.accept("Tabc;", null);
+    consumer.accept("Ta-b;", null);
+    consumer.accept("Ta-b-c;", null);
+  }
+
+  public void forArrayTypeSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    StringBuilder arrayPrefix = new StringBuilder();
+    for (int i = 0; i < 3; i++) {
+      arrayPrefix.append("[");
+      forBasicTypes(t -> consumer.accept(arrayPrefix.toString() + t, null));
+      forClassSignatures((x, y) -> {
+        consumer.accept(arrayPrefix.toString() + x, y);
+      });
+      forTypeVariableSignatures((x, y) -> {
+        consumer.accept(arrayPrefix.toString() + x, y);
+      });
+      //consumer.accept();
+    }
+  }
+
+  public void forClassTypeSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    // In https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.4 (Java 7) it
+    // says: "If the class bound does not specify a type, it is taken to be Object.". That sentence
+    // is not present for Java 8 or Java 9. We do test it here, and javac from OpenJDK 8 will also
+    // produce that for a class that extends Object and implements at least one interface.
+
+    // class C<T> { ... }.
+    consumer.accept("<T:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C<T,U> { ... }.
+    consumer.accept("<T:U:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T>> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList<TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends List> { ... }.
+    consumer.accept("<T::Ljava/util/List;>Ljava/lang/Object;", null);
+    // class C<T extends List<T>> { ... }.
+    consumer.accept("<T::Ljava/util/List<TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends List<T[]>> { ... }.
+    consumer.accept("<T::Ljava/util/List<[TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends List<T[][]>> { ... }.
+    consumer.accept("<T::Ljava/util/List<[[TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList & List> { ... }.
+    consumer.accept("<T:Ljava/util/AbstractList;:Ljava/util/List;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T> & List<T>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<TT;>;:Ljava/util/List<TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T[]> & List<T[]>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<[TT;>;:Ljava/util/List<[TT;>;>Ljava/lang/Object;", null);
+    // class C<T extends AbstractList<T[][]> & List<T[][]>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<[[TT;>;:Ljava/util/List<[[TT;>;>Ljava/lang/Object;", null);
+
+    // class C<T extends AbstractList & List & Iterator> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList;:Ljava/util/List;:Ljava/util/Iterator;>Ljava/lang/Object;",
+        null);
+    // class C<T extends AbstractList<T> & List<T> & Iterator<T>> { ... }.
+    consumer.accept(
+        "<T:Ljava/util/AbstractList<TT;>;:Ljava/util/List<TT;>;:Ljava/util/Iterator<TT;>;>"
+            + "Ljava/lang/Object;", null);
+
+    // class C<T,U> { ... }.
+    consumer.accept("<T:U:>Ljava/lang/Object;", null);
+    consumer.accept("<T:Ljava/lang/Object;U:Ljava/lang/Object;>Ljava/lang/Object;", null);
+
+    // class C extends java.util.AbstractList<String>
+    consumer.accept("Ljava/util/AbstractList<Ljava/lang/String;>;", null);
+    // class C extends java.util.AbstractList<String> implements List<String>
+    consumer.accept(
+        "Ljava/util/AbstractList<Ljava/lang/String;>;Ljava/util/List<Ljava/lang/String;>;",
+        ImmutableSet.of(44));
+    // class C extends java.util.AbstractList<String> implements List<String>, Iterator<String>
+    consumer.accept(
+        "Ljava/util/AbstractList<Ljava/lang/String;>;"
+            + "Ljava/util/List<Ljava/lang/String;>;Ljava/util/Iterator<Ljava/lang/String;>;",
+        ImmutableSet.of(44, 80));
+
+    // class C<T> extends java.util.AbstractList<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>;", null);
+    // class C<T> extends java.util.AbstractList<T> implements List<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>;Ljava/util/List<TT;>;",
+        ImmutableSet.of(51));
+    // class C<T> extends java.util.AbstractList<T> implements List<T>, Iterator<T>
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/util/AbstractList<TT;>"
+            + ";Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;",
+        ImmutableSet.of(51, 72));
+
+    // class Outer<T> {
+    //   class Inner {
+    //     class InnerInner {
+    //     }
+    //     class ExtendsInnerInner extends InnerInner {
+    //     }
+    //   }
+    //   class ExtendsInner extends Inner {
+    //   }
+    // }
+    consumer.accept("<T:Ljava/lang/Object;>Ljava/lang/Object;", null);  // Outer signature.
+    // Inner has no signature.
+    // InnerInner has no signature.
+    consumer.accept("LOuter<TT;>.Inner.InnerInner;", null);  // ExtendsInnerInner signature.
+    consumer.accept("LOuter<TT;>.Inner;", null);  // ExtendsInner signature.
+
+    // class Outer<T> {
+    //   class Inner<T> {
+    //   }
+    //   interface InnerInterface<T> {
+    //   }
+    //   abstract class ExtendsInner<U> extends Inner implements InnerInterface<U> {
+    //   }
+    // }
+    consumer.accept(
+        "<U:Ljava/lang/Object;>LOuter<TT;>.Inner<TT;>;LOuter$InnerInterface<TU;>;",
+        ImmutableSet.of(45));
+  }
+
+  public void forMethodSignatures(BiConsumer<String, Set<Integer>> consumer) {
+    forBasicTypesAndVoid(t -> consumer.accept("()" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:Ljava/util/List;>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(t -> consumer.accept("<T:U:>(BCDFIJSZ)" + t, null));
+    forBasicTypesAndVoid(
+        t -> consumer.accept("<T:Ljava/util/List;U:Ljava/util/List;>(BCDFIJSZ)" + t, null));
+
+    consumer.accept("<T:U:>(Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;)V", null);
+    consumer.accept(
+        "<T:U:>(Ljava/util/List<TT;>;Ljava/util/Iterator<TT;>;)Ljava/util/List<TT;>;", null);
+
+    consumer.accept("<T:U:>(La/b/c<TT;>.i<TT;>;)V", null);
+    consumer.accept("<T:U:>(La/b/c<TT;>.i<TT;>.j<TU;>;)V", null);
+
+    consumer.accept("<T:>()La/b/c<TT;>;", null);
+    consumer.accept("<T:>()La/b/c<TT;>.i<TT;>;", null);
+    consumer.accept("<T:>()La/b/c<TT;>.i<TT;>.j<TT;>;", null);
+  }
+
+  @Test
+  public void testClassSignatureGenericClass() {
+    forClassTypeSignatures(this::parseClassSignature);
+  }
+
+  @Test
+  public void parseFieldSignature() {
+    forClassSignatures(this::parseFieldSignature);
+    forTypeVariableSignatures(this::parseFieldSignature);
+    forArrayTypeSignatures(this::parseFieldSignature);
+  }
+
+  @Test
+  public void parseMethodSignature() {
+    forMethodSignatures(this::parseMethodSignature);
+  }
+
+  private void failingParseAction(
+      Consumer<GenericSignatureParser<String>> parse, String errorMessageType)
+      throws Exception {
+    class ThrowsInParserActionBase<E extends Error> extends ReGenerateGenericSignatureRewriter {
+      protected Supplier<? extends E> exceptionSupplier;
+
+      private ThrowsInParserActionBase(Supplier<? extends E> exceptionSupplier) {
+        this.exceptionSupplier = exceptionSupplier;
+      }
+    }
+
+    class ThrowsInParsedSymbol<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedSymbol(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void parsedSymbol(char symbol) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedIdentifier<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedIdentifier(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void parsedIdentifier(String identifier) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedTypeName<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedTypeName(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public String parsedTypeName(String name) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInParsedInnerTypeName<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInParsedInnerTypeName(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public String parsedInnerTypeName(String enclosingType, String name) {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInStart<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInStart(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void start() {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    class ThrowsInStop<E extends Error> extends ThrowsInParserActionBase<E> {
+      private ThrowsInStop(Supplier<? extends E> exceptionSupplier) {
+        super(exceptionSupplier);
+      }
+
+      @Override
+      public void stop() {
+        throw exceptionSupplier.get();
+      }
+    }
+
+    List<GenericSignatureAction<String>> throwingActions = ImmutableList.of(
+        new ThrowsInParsedSymbol<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedSymbol<>(() -> new Error("ERROR")),
+        new ThrowsInParsedIdentifier<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedIdentifier<>(() -> new Error("ERROR")),
+        new ThrowsInParsedTypeName<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedTypeName<>(() -> new Error("ERROR")),
+        new ThrowsInParsedInnerTypeName<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInParsedInnerTypeName<>(() -> new Error("ERROR")),
+        new ThrowsInStart<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInStart<>(() -> new Error("ERROR")),
+        new ThrowsInStop<>(() -> new GenericSignatureFormatError("ERROR")),
+        new ThrowsInStop<>(() -> new Error("ERROR")));
+
+    int plainErrorCount = 0;
+    for (GenericSignatureAction<String> action : throwingActions) {
+      GenericSignatureParser<String> parser = new GenericSignatureParser<>(action);
+      try {
+        // This class signature hits all action callbacks.
+        parse.accept(parser);
+        fail("Parse succeeded for " + action.getClass().getSimpleName());
+      } catch (GenericSignatureFormatError e) {
+        if (e.getSuppressed().length == 0) {
+          assertEquals("ERROR", e.getMessage());
+        } else {
+          plainErrorCount++;
+          assertEquals("Unknown error parsing "
+              + errorMessageType + " signature: ERROR", e.getMessage());
+        }
+      }
+    }
+    assertEquals(6, plainErrorCount);
+  }
+
+  @Test
+  public void failingParseAction() throws Exception {
+    // These signatures hits all action callbacks.
+    failingParseAction(parser -> parser.parseClassSignature(
+        "<U:Ljava/lang/Object;>LOuter<TT;>.Inner;Ljava/util/List<TU;>;"), "class");
+    failingParseAction(
+        parser -> parser.parseFieldSignature("LOuter$InnerInterface<TU;>.Inner;"), "field");
+    failingParseAction(
+        parser -> parser.parseMethodSignature("(LOuter$InnerInterface<TU;>.Inner;)V"), "method");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
new file mode 100644
index 0000000..b9ff7ea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
@@ -0,0 +1,536 @@
+// Copyright (c) 2018, 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;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+public class MinifierClassSignatureTest extends TestBase {
+  /*
+
+  class Simple {
+  }
+  class Base<T> {
+  }
+  class Outer<T> {
+    class Inner {
+      class InnerInner {
+      }
+      class ExtendsInnerInner extends InnerInner {
+      }
+    }
+    class ExtendsInner extends Inner {
+    }
+  }
+
+  */
+
+  String baseSignature = "<T:Ljava/lang/Object;>Ljava/lang/Object;";
+  String outerSignature = "<T:Ljava/lang/Object;>Ljava/lang/Object;";
+  String extendsInnerSignature = "LOuter<TT;>.Inner;";
+  String extendsInnerInnerSignature = "LOuter<TT;>.Inner.InnerInner;";
+
+  private byte[] dumpSimple(String classSignature) throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+
+    String signature = classSignature;
+    cw.visit(V1_8, ACC_SUPER, "Simple", signature, "java/lang/Object", null);
+
+    {
+      mv = cw.visitMethod(0, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpBase(String classSignature) throws Exception {
+
+    final String javacClassSignature = baseSignature;
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Base", signature, "java/lang/Object", null);
+
+    {
+      mv = cw.visitMethod(0, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+
+  private byte[] dumpOuter(String classSignature) {
+
+    final String javacClassSignature = outerSignature;
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Outer", signature, "java/lang/Object", null);
+
+    cw.visitInnerClass("Outer$ExtendsInner", "Outer", "ExtendsInner", 0);
+
+    cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+    {
+      mv = cw.visitMethod(0, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpInner(String classSignature) {
+
+    final String javacClassSignature = null;
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Outer$Inner", signature, "java/lang/Object", null);
+
+    cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+    cw.visitInnerClass("Outer$Inner$ExtendsInnerInner", "Outer$Inner", "ExtendsInnerInner", 0);
+
+    cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LOuter;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LOuter;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Outer$Inner", "this$0", "LOuter;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpExtendsInner(String classSignature) throws Exception {
+
+    final String javacClassSignature = extendsInnerSignature;
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Outer$ExtendsInner", signature, "Outer$Inner", null);
+
+    cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+    cw.visitInnerClass("Outer$ExtendsInner", "Outer", "ExtendsInner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LOuter;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LOuter;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Outer$ExtendsInner", "this$0", "LOuter;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitMethodInsn(INVOKESPECIAL, "Outer$Inner", "<init>", "(LOuter;)V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpInnerInner(String classSignature) throws Exception {
+
+    final String javacClassSignature = null;
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Outer$Inner$InnerInner", signature, "java/lang/Object", null);
+
+    cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+    cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$1", "LOuter$Inner;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LOuter$Inner;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Outer$Inner$InnerInner", "this$1", "LOuter$Inner;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpExtendsInnerInner(String classSignature) throws Exception {
+
+    final String javacClassSignature = extendsInnerInnerSignature;
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    String signature = classSignature != null ? classSignature : javacClassSignature;
+    cw.visit(V1_8, ACC_SUPER, "Outer$Inner$ExtendsInnerInner", signature, "Outer$Inner$InnerInner",
+        null);
+
+    cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+    cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+    cw.visitInnerClass("Outer$Inner$ExtendsInnerInner", "Outer$Inner", "ExtendsInnerInner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$1", "LOuter$Inner;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LOuter$Inner;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Outer$Inner$ExtendsInnerInner", "this$1", "LOuter$Inner;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitMethodInsn(INVOKESPECIAL, "Outer$Inner$InnerInner", "<init>", "(LOuter$Inner;)V",
+          false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  public void runTest(
+      ImmutableMap<String, String> signatures,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspect)
+      throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    DexInspector inspector = new DexInspector(
+      ToolHelper.runR8(R8Command.builder(checker)
+          .addClassProgramData(dumpSimple(signatures.get("Simple")), Origin.unknown())
+          .addClassProgramData(dumpBase(signatures.get("Base")), Origin.unknown())
+          .addClassProgramData(dumpOuter(signatures.get("Outer")), Origin.unknown())
+          .addClassProgramData(dumpInner(signatures.get("Outer$Inner")), Origin.unknown())
+          .addClassProgramData(
+              dumpExtendsInner(signatures.get("Outer$ExtendsInner")), Origin.unknown())
+          .addClassProgramData(
+              dumpInnerInner(signatures.get("Outer$Inner$InnerInner")), Origin.unknown())
+          .addClassProgramData(
+              dumpExtendsInnerInner(
+                  signatures.get("Outer$Inner$ExtendsInnerInner")), Origin.unknown())
+          .addProguardConfiguration(ImmutableList.of(
+              "-keepattributes InnerClasses,EnclosingMethod,Signature",
+              "-keep,allowobfuscation class **"
+          ), Origin.unknown())
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .setProguardMapConsumer(StringConsumer.emptyConsumer())
+          .build()));
+    // All classes are kept, and renamed.
+    assertThat(inspector.clazz("Simple"), isRenamed());
+    assertThat(inspector.clazz("Base"), isRenamed());
+    assertThat(inspector.clazz("Outer"), isRenamed());
+    assertThat(inspector.clazz("Outer$Inner"), isRenamed());
+    assertThat(inspector.clazz("Outer$ExtendsInner"), isRenamed());
+    assertThat(inspector.clazz("Outer$Inner$InnerInner"), isRenamed());
+    assertThat(inspector.clazz("Outer$Inner$ExtendsInnerInner"), isRenamed());
+
+    // Test that classes with have their original signature if the default was provided.
+    if (!signatures.containsKey("Simple")) {
+      assertNull(inspector.clazz("Simple").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Base")) {
+      assertEquals(baseSignature, inspector.clazz("Base").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Outer")) {
+      assertEquals(outerSignature, inspector.clazz("Outer").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Outer$Inner")) {
+      assertNull(inspector.clazz("Outer$Inner").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Outer$ExtendsInner")) {
+      assertEquals(extendsInnerSignature,
+          inspector.clazz("Outer$ExtendsInner").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Outer$Inner$InnerInner")) {
+      assertNull(inspector.clazz("Outer$Inner$InnerInner").getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("Outer$Inner$ExtendsInnerInner")) {
+      assertEquals(extendsInnerInnerSignature,
+          inspector.clazz("Outer$Inner$ExtendsInnerInner").getOriginalSignatureAttribute());
+    }
+
+    diagnostics.accept(checker);
+    inspect.accept(inspector);
+  }
+
+  private void testSingleClass(String name, String signature,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspector)
+      throws Exception {
+    ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+    runTest(signatures, diagnostics, inspector);
+  }
+
+  private void isOriginUnknown(Origin origin) {
+    assertSame(Origin.unknown(), origin);
+  }
+
+  private void noWarnings(DiagnosticsChecker checker) {
+    assertEquals(0, checker.warnings.size());
+  }
+
+  private void noInspection(DexInspector inspector) {
+  }
+
+  private void noSignatureAttribute(ClassSubject clazz) {
+    assertNull(clazz.getFinalSignatureAttribute());
+    assertNull(clazz.getOriginalSignatureAttribute());
+  }
+
+  @Test
+  public void originalJavacSignatures() throws Exception {
+    // Test using the signatures generated by javac.
+    runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+  }
+
+  @Test
+  public void classSignature_empty() throws Exception {
+    testSingleClass("Outer", "", this::noWarnings, inspector -> {
+      ClassSubject outer = inspector.clazz("Outer");
+      assertNull(outer.getFinalSignatureAttribute());
+      assertNull(outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureOuter_valid() throws Exception {
+    // class Outer<T extends Simple> extends Base<T>
+    String signature = "<T:LSimple;>LBase<TT;>;";
+    testSingleClass("Outer", signature, this::noWarnings, inspector -> {
+      ClassSubject outer = inspector.clazz("Outer");
+      ClassSubject simple = inspector.clazz("Simple");
+      ClassSubject base = inspector.clazz("Base");
+      String baseDescriptorWithoutSemicolon =
+          base.getFinalDescriptor().substring(0, base.getFinalDescriptor().length() - 1);
+      String minifiedSignature =
+          "<T:" +  simple.getFinalDescriptor() + ">" + baseDescriptorWithoutSemicolon + "<TT;>;";
+      assertEquals(minifiedSignature, outer.getFinalSignatureAttribute());
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureExtendsInner_valid() throws Exception {
+    String signature = "LOuter<TT;>.Inner;";
+    testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+      ClassSubject extendsInner = inspector.clazz("Outer$ExtendsInner");
+      ClassSubject outer = inspector.clazz("Outer");
+      ClassSubject inner = inspector.clazz("Outer$Inner");
+      String outerDescriptorWithoutSemicolon =
+          outer.getFinalDescriptor().substring(0, outer.getFinalDescriptor().length() - 1);
+      String innerFinalDescriptor = inner.getFinalDescriptor();
+      String innerLastPart =
+          innerFinalDescriptor.substring(innerFinalDescriptor.indexOf("$") + 1);
+      String minifiedSignature = outerDescriptorWithoutSemicolon + "<TT;>." + innerLastPart;
+      assertEquals(minifiedSignature, extendsInner.getFinalSignatureAttribute());
+      assertEquals(signature, extendsInner.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureOuter_classNotFound() throws Exception {
+    String signature = "<T:LNotFound;>LAlsoNotFound;";
+    testSingleClass("Outer", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer");
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureExtendsInner_innerClassNotFound() throws Exception {
+    String signature = "LOuter<TT;>.NotFound;";
+    testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureExtendsInner_outerAndInnerClassNotFound() throws Exception {
+    String signature = "LNotFound<TT;>.AlsoNotFound;";
+    testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureExtendsInner_nestedInnerClassNotFound() throws Exception {
+    String signature = "LOuter<TT;>.Inner.NotFound;";
+    testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureExtendsInner_multipleMestedInnerClassesNotFound() throws Exception {
+    String signature = "LOuter<TT;>.NotFound.AlsoNotFound;";
+    testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+      assertEquals(signature, outer.getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void classSignatureOuter_invalid() throws Exception {
+    testSingleClass("Outer", "X", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for class Outer", "Expected L at position 1");
+    }, inspector -> noSignatureAttribute(inspector.clazz("Outer")));
+  }
+
+  @Test
+  public void classSignatureOuter_invalidEnd() throws Exception {
+    testSingleClass("Outer", "<L", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for class Outer", "Unexpected end of signature at position 3");
+    }, inspector -> noSignatureAttribute(inspector.clazz("Outer")));
+  }
+
+  @Test
+  public void classSignatureExtendsInner_invalid() throws Exception {
+    testSingleClass("Outer$ExtendsInner", "X", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for class Outer$ExtendsInner", "Expected L at position 1");
+    }, inspector -> noSignatureAttribute(inspector.clazz("Outer$ExtendsInner")));
+  }
+
+  @Test
+  public void classSignatureExtendsInnerInner_invalid() throws Exception {
+    testSingleClass("Outer$Inner$ExtendsInnerInner", "X", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for class Outer$Inner$ExtendsInnerInner",
+          "Expected L at position 1");
+    }, inspector -> noSignatureAttribute(inspector.clazz("Outer$Inner$ExtendsInnerInner")));
+  }
+
+  @Test
+  public void multipleWarnings() throws Exception {
+    runTest(ImmutableMap.of(
+        "Outer", "X",
+        "Outer$ExtendsInner", "X",
+        "Outer$Inner$ExtendsInnerInner", "X"), diagnostics -> {
+      assertEquals(3, diagnostics.warnings.size());
+    }, inspector -> {
+      noSignatureAttribute(inspector.clazz("Outer"));
+      noSignatureAttribute(inspector.clazz("Outer$ExtendsInner"));
+      noSignatureAttribute(inspector.clazz("Outer$Inner$ExtendsInnerInner"));
+    });
+  }
+  @Test
+  public void regress80029761() throws Exception {
+    String signature = "LOuter<TT;>.com/example/Inner;";
+    testSingleClass("Outer$ExtendsInner", signature, diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for class Outer$ExtendsInner", "Expected ; at position 16");
+    }, inspector -> {
+      noSignatureAttribute(inspector.clazz("Outer$ExtendsInner"));
+    });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java
new file mode 100644
index 0000000..e48ce56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java
@@ -0,0 +1,289 @@
+// Copyright (c) 2018, 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;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.FieldSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+
+public class MinifierFieldSignatureTest extends TestBase {
+  /*
+
+  class Fields<X extends String> {
+    class Inner {
+    }
+    public X anX;
+    public X[] anArrayOfX;
+    public Fields<X> aFieldsOfX;
+    public Fields<X>.Inner aFieldsOfXInner;
+  }
+
+  */
+
+  private String anXSignature = "TX;";
+  private String anArrayOfXSignature = "[TX;";
+  private String aFieldsOfXSignature = "LFields<TX;>;";
+  private String aFieldsOfXInnerSignature = "LFields<TX;>.Inner;";
+
+  public byte[] dumpFields(Map<String, String> signatures) throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    String signature;
+
+    cw.visit(V1_8, ACC_SUPER, "Fields", "<X:Ljava/lang/String;>Ljava/lang/Object;",
+        "java/lang/Object", null);
+
+    cw.visitInnerClass("Fields$Inner", "Fields", "Inner", 0);
+
+    {
+      signature = signatures.get("anX");
+      signature = signature == null ? anXSignature : signature;
+      fv = cw.visitField(ACC_PUBLIC, "anX", "Ljava/lang/String;", signature, null);
+      fv.visitEnd();
+    }
+    {
+      signature = signatures.get("anArrayOfX");
+      signature = signature == null ? anArrayOfXSignature : signature;
+      fv = cw.visitField(
+          ACC_PUBLIC, "anArrayOfX", "[Ljava/lang/String;", signature, null);
+      fv.visitEnd();
+    }
+    {
+      signature = signatures.get("aFieldsOfX");
+      signature = signature == null ? aFieldsOfXSignature : signature;
+      fv = cw.visitField(ACC_PUBLIC, "aFieldsOfX", "LFields;", signature, null);
+      fv.visitEnd();
+    }
+    {
+      signature = signatures.get("aFieldsOfXInner");
+      signature = signature == null ? aFieldsOfXInnerSignature : signature;
+      fv = cw.visitField(
+          ACC_PUBLIC, "aFieldsOfXInner", "LFields$Inner;", signature, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  public byte[] dumpInner() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    cw.visit(V1_8, ACC_SUPER, "Fields$Inner", null, "java/lang/Object", null);
+
+    cw.visitInnerClass("Fields$Inner", "Fields", "Inner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LFields;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LFields;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Fields$Inner", "this$0", "LFields;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private FieldSubject lookupAnX(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Fields");
+    return clazz.field("java.lang.String", "anX");
+  }
+
+  private FieldSubject lookupAnArrayOfX(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Fields");
+    return clazz.field("java.lang.String[]", "anArrayOfX");
+  }
+
+  private FieldSubject lookupAFieldsOfX(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Fields");
+    return clazz.field("Fields", "aFieldsOfX");
+  }
+
+  public void runTest(
+      ImmutableMap<String, String> signatures,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspect)
+      throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    DexInspector inspector = new DexInspector(
+      ToolHelper.runR8(R8Command.builder(checker)
+          .addClassProgramData(dumpFields(signatures), Origin.unknown())
+          .addClassProgramData(dumpInner(), Origin.unknown())
+          .addProguardConfiguration(ImmutableList.of(
+              "-keepattributes InnerClasses,EnclosingMethod,Signature",
+              "-keep,allowobfuscation class ** { *; }"
+          ), Origin.unknown())
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .setProguardMapConsumer(StringConsumer.emptyConsumer())
+          .build()));
+    // All classes are kept, and renamed.
+    ClassSubject clazz = inspector.clazz("Fields");
+    assertThat(clazz, isRenamed());
+    assertThat(inspector.clazz("Fields$Inner"), isRenamed());
+
+    FieldSubject anX = lookupAnX(inspector);
+    FieldSubject anArrayOfX = lookupAnArrayOfX(inspector);
+    FieldSubject aFieldsOfX =lookupAFieldsOfX(inspector);
+    FieldSubject aFieldsOfXInner = clazz.field("Fields$Inner", "aFieldsOfXInner");
+
+    // Check that all fields have been renamed
+    assertThat(anX, isRenamed());
+    assertThat(anArrayOfX, isRenamed());
+    assertThat(aFieldsOfX, isRenamed());
+    assertThat(aFieldsOfXInner, isRenamed());
+
+    //System.out.println(generic.getFinalSignatureAttribute());
+    //System.out.println(generic.getOriginalSignatureAttribute());
+
+    // Test that methods have their original signature if the default was provided.
+    if (!signatures.containsKey("anX")) {
+      assertEquals(anXSignature, anX.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("anArrayOfX")) {
+      assertEquals(anArrayOfXSignature, anArrayOfX.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("aFieldsOfX")) {
+      assertEquals(
+          aFieldsOfXSignature, aFieldsOfX.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("aFieldsOfXInner")) {
+      assertEquals(
+          aFieldsOfXInnerSignature, aFieldsOfXInner.getOriginalSignatureAttribute());
+    }
+
+    diagnostics.accept(checker);
+    inspect.accept(inspector);
+  }
+
+  private void testSingleField(String name, String signature,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspector)
+      throws Exception {
+    ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+    runTest(signatures, diagnostics, inspector);
+  }
+
+  private void isOriginUnknown(Origin origin) {
+    assertSame(Origin.unknown(), origin);
+  }
+
+  private void noWarnings(DiagnosticsChecker checker) {
+    assertEquals(0, checker.warnings.size());
+  }
+
+  private void noInspection(DexInspector inspector) {
+  }
+
+  private void noSignatureAttribute(FieldSubject field) {
+    assertThat(field, isPresent());
+    assertNull(field.getFinalSignatureAttribute());
+    assertNull(field.getOriginalSignatureAttribute());
+  }
+
+  @Test
+  public void originalJavacSignatures() throws Exception {
+    // Test using the signatures generated by javac.
+    runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+  }
+
+  @Test
+  public void signatureEmpty() throws Exception {
+    testSingleField("anX", "", this::noWarnings,
+        inspector -> noSignatureAttribute(lookupAnX(inspector)));
+  }
+
+  @Test
+  public void signatureInvalid() throws Exception {
+    testSingleField("anX", "X", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      // TODO(sgjesse): The position 2 reported here is one off.
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for field",
+          "java.lang.String Fields.anX",
+          "Expected L, [ or T at position 2");
+    }, inspector -> noSignatureAttribute(lookupAnX(inspector)));
+  }
+
+  @Test
+  public void classNotFound() throws Exception {
+    String signature = "LNotFound<TX;>.InnerNotFound.InnerAlsoNotFound;";
+    testSingleField("anX", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      assertEquals(signature, lookupAnX(inspector).getOriginalSignatureAttribute());
+    });
+  }
+
+  @Test
+  public void multipleWarnings() throws Exception {
+    runTest(ImmutableMap.of(
+        "anX", "X",
+        "anArrayOfX", "X",
+        "aFieldsOfX", "X"
+    ), diagnostics -> {
+      assertEquals(3, diagnostics.warnings.size());
+    }, inspector -> {
+      noSignatureAttribute(lookupAnX(inspector));
+      noSignatureAttribute(lookupAnArrayOfX(inspector));
+      noSignatureAttribute(lookupAFieldsOfX(inspector));
+    });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java
new file mode 100644
index 0000000..41750ae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java
@@ -0,0 +1,314 @@
+// Copyright (c) 2018, 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;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+public class MinifierMethodSignatureTest extends TestBase {
+  /*
+
+  class Methods<X extends Throwable> {
+    class Inner {
+    }
+    public static <T extends Throwable> T generic(T a, Methods<T>.Inner b) { return null; }
+    public Methods<X>.Inner parameterizedReturn() { return null; }
+    public void parameterizedArguments(X a, Methods<X>.Inner b) { }
+    public void parametrizedThrows() throws X { }
+  }
+
+  */
+
+  private String genericSignature = "<T:Ljava/lang/Throwable;>(TT;LMethods<TT;>.Inner;)TT;";
+  private String parameterizedReturnSignature = "()LMethods<TX;>.Inner;";
+  private String parameterizedArgumentsSignature = "(TX;LMethods<TX;>.Inner;)V";
+  private String parametrizedThrowsSignature = "()V^TX;";
+
+  private byte[] dumpMethods(Map<String, String> signatures) throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+    String signature;
+
+    cw.visit(V1_8, ACC_SUPER, "Methods", "<X:Ljava/lang/Throwable;>Ljava/lang/Object;",
+        "java/lang/Object", null);
+
+    cw.visitInnerClass("Methods$Inner", "Methods", "Inner", 0);
+
+    {
+      mv = cw.visitMethod(0, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      signature = signatures.get("generic");
+      signature = signature == null ? genericSignature : signature;
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "generic",
+          "(Ljava/lang/Throwable;LMethods$Inner;)Ljava/lang/Throwable;",
+          signature, null);
+      mv.visitCode();
+      mv.visitInsn(ACONST_NULL);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(1, 2);
+      mv.visitEnd();
+    }
+    {
+      signature = signatures.get("parameterizedReturn");
+      signature = signature == null ? parameterizedReturnSignature : signature;
+      mv = cw.visitMethod(ACC_PUBLIC, "parameterizedReturn", "()LMethods$Inner;",
+          signature, null);
+      mv.visitCode();
+      mv.visitInsn(ACONST_NULL);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      signature = signatures.get("parameterizedArguments");
+      signature = signature == null ? parameterizedArgumentsSignature : signature;
+      mv = cw.visitMethod(ACC_PUBLIC, "parameterizedArguments",
+          "(Ljava/lang/Throwable;LMethods$Inner;)V", signature, null);
+      mv.visitCode();
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(0, 3);
+      mv.visitEnd();
+    }
+    {
+      signature = signatures.get("parametrizedThrows");
+      signature = signature == null ? parametrizedThrowsSignature : signature;
+      mv = cw.visitMethod(ACC_PUBLIC, "parametrizedThrows", "()V", signature,
+          new String[] { "java/lang/Throwable" });
+      mv.visitCode();
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(0, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private byte[] dumpInner() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+
+    cw.visit(V1_8, ACC_SUPER, "Methods$Inner", null, "java/lang/Object", null);
+
+    cw.visitInnerClass("Methods$Inner", "Methods", "Inner", 0);
+
+    {
+      fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LMethods;", null, null);
+      fv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(0, "<init>", "(LMethods;)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "Methods$Inner", "this$0", "LMethods;");
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private MethodSubject lookupGeneric(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Methods");
+    return clazz.method(
+        "java.lang.Throwable", "generic", ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+  }
+
+  private MethodSubject lookupParameterizedReturn(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Methods");
+    return clazz.method(
+        "Methods$Inner", "parameterizedReturn", ImmutableList.of());
+  }
+
+  private MethodSubject lookupParameterizedArguments(DexInspector inspector) {
+    ClassSubject clazz = inspector.clazz("Methods");
+    return clazz.method(
+        "void", "parameterizedArguments", ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+  }
+
+  public void runTest(
+      ImmutableMap<String, String> signatures,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspect)
+      throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    DexInspector inspector = new DexInspector(
+      ToolHelper.runR8(R8Command.builder(checker)
+          .addClassProgramData(dumpMethods(signatures), Origin.unknown())
+          .addClassProgramData(dumpInner(), Origin.unknown())
+          .addProguardConfiguration(ImmutableList.of(
+              "-keepattributes InnerClasses,EnclosingMethod,Signature",
+              "-keep,allowobfuscation class ** { *; }"
+          ), Origin.unknown())
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .setProguardMapConsumer(StringConsumer.emptyConsumer())
+          .build()));
+    // All classes are kept, and renamed.
+    ClassSubject clazz = inspector.clazz("Methods");
+    assertThat(clazz, isRenamed());
+    assertThat(inspector.clazz("Methods$Inner"), isRenamed());
+
+    MethodSubject generic = lookupGeneric(inspector);
+    MethodSubject parameterizedReturn = lookupParameterizedReturn(inspector);
+    MethodSubject parameterizedArguments = lookupParameterizedArguments(inspector);
+    MethodSubject parametrizedThrows =
+        clazz.method("void", "parametrizedThrows", ImmutableList.of());
+
+    // Check that all methods have been renamed
+    assertThat(generic, isRenamed());
+    assertThat(parameterizedReturn, isRenamed());
+    assertThat(parameterizedArguments, isRenamed());
+    assertThat(parametrizedThrows, isRenamed());
+
+    // Test that methods have their original signature if the default was provided.
+    if (!signatures.containsKey("generic")) {
+      assertEquals(genericSignature, generic.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("parameterizedReturn")) {
+      assertEquals(
+          parameterizedReturnSignature, parameterizedReturn.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("parameterizedArguments")) {
+      assertEquals(
+          parameterizedArgumentsSignature, parameterizedArguments.getOriginalSignatureAttribute());
+    }
+    if (!signatures.containsKey("parametrizedThrows")) {
+      assertEquals(
+          parametrizedThrowsSignature, parametrizedThrows.getOriginalSignatureAttribute());
+    }
+
+    diagnostics.accept(checker);
+    inspect.accept(inspector);
+  }
+
+  private void testSingleMethod(String name, String signature,
+      Consumer<DiagnosticsChecker> diagnostics,
+      Consumer<DexInspector> inspector)
+      throws Exception {
+    ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+    runTest(signatures, diagnostics, inspector);
+  }
+
+  private void isOriginUnknown(Origin origin) {
+    assertSame(Origin.unknown(), origin);
+  }
+
+  private void noWarnings(DiagnosticsChecker checker) {
+    assertEquals(0, checker.warnings.size());
+  }
+
+  private void noInspection(DexInspector inspector) {
+  }
+
+  private void noSignatureAttribute(MethodSubject method) {
+    assertThat(method, isPresent());
+    assertNull(method.getFinalSignatureAttribute());
+    assertNull(method.getOriginalSignatureAttribute());
+  }
+
+  @Test
+  public void originalJavacSignatures() throws Exception {
+    // Test using the signatures generated by javac.
+    runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+  }
+
+  @Test
+  public void signatureEmpty() throws Exception {
+    testSingleMethod("generic", "", this::noWarnings, inspector -> {
+      noSignatureAttribute(lookupGeneric(inspector));
+    });
+  }
+
+  @Test
+  public void signatureInvalid() throws Exception {
+    testSingleMethod("generic", "X", diagnostics -> {
+      assertEquals(1, diagnostics.warnings.size());
+      DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+          "Invalid signature for method",
+          "java.lang.Throwable Methods.generic(java.lang.Throwable, Methods$Inner)",
+          "Expected ( at position 1");
+    }, inspector -> noSignatureAttribute(lookupGeneric(inspector)));
+  }
+
+  @Test
+  public void classNotFound() throws Exception {
+    String signature = "<T:LNotFound;>(TT;LAlsoNotFound<TT;>.InnerNotFound.InnerAlsoNotFound;)TT;";
+    testSingleMethod("generic", signature, this::noWarnings,
+        inspector -> {
+          ClassSubject methods = inspector.clazz("Methods");
+          MethodSubject method =
+              methods.method("java.lang.Throwable", "generic",
+                  ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+          assertThat(inspector.clazz("NotFound"), not(isPresent()));
+          assertEquals(signature, method.getOriginalSignatureAttribute());
+        });
+  }
+
+  @Test
+  public void multipleWarnings() throws Exception {
+    runTest(ImmutableMap.of(
+        "generic", "X",
+        "parameterizedReturn", "X",
+        "parameterizedArguments", "X"
+    ), diagnostics -> {
+      assertEquals(3, diagnostics.warnings.size());
+    }, inspector -> {
+      noSignatureAttribute(lookupGeneric(inspector));
+      noSignatureAttribute(lookupParameterizedReturn(inspector));
+      noSignatureAttribute(lookupParameterizedArguments(inspector));
+    });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
index d9808c0..f311c15 100644
--- a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
@@ -5,9 +5,9 @@
 
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
-import com.android.tools.r8.graph.ClassAndMemberPublicizer;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
index c82bbc9..9a9ca34 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
@@ -4,52 +4,303 @@
 
 package com.android.tools.r8.naming.b72391662;
 
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
 
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.VmTestRunner;
 import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
 import com.android.tools.r8.naming.b72391662.subpackage.OtherPackageSuper;
 import com.android.tools.r8.naming.b72391662.subpackage.OtherPackageTestClass;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(VmTestRunner.class)
-public class B72391662 extends TestBase {
+public class B72391662 extends ProguardCompatabilityTestBase {
 
-  private void doTest(boolean allowAccessModification, boolean minify) throws Exception {
+  private static final List<Class> CLASSES = ImmutableList.of(
+      TestMain.class, Interface.class, Super.class, TestClass.class,
+      OtherPackageSuper.class, OtherPackageTestClass.class);
+
+  private void doTest_keepAll(
+      Shrinker shrinker,
+      String repackagePrefix,
+      boolean allowAccessModification,
+      boolean minify) throws Exception {
     Class mainClass = TestMain.class;
+    String keep = !minify ? "-keep" : "-keep,allowobfuscation";
     List<String> config = ImmutableList.of(
-        allowAccessModification ?"-allowaccessmodification" : "",
+        "-printmapping",
+        repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+        allowAccessModification ? "-allowaccessmodification" : "",
         !minify ? "-dontobfuscate" : "",
         "-keep class " + mainClass.getCanonicalName() + " {",
         "  public void main(java.lang.String[]);",
         "}",
-        "-keep class " + TestClass.class.getCanonicalName() + " {",
+        keep + " class " + TestClass.class.getCanonicalName() + " {",
         "  *;",
         "}",
-        "-keep class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+        keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
         "  *;",
-        "}"
+        "}",
+        "-dontwarn java.lang.invoke.*"
     );
 
-    AndroidApp app = readClassesAndAndriodJar(ImmutableList.of(
-        mainClass, Interface.class, Super.class, TestClass.class,
-        OtherPackageSuper.class, OtherPackageTestClass.class));
-    app = compileWithR8(app, String.join(System.lineSeparator(), config));
+    AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
     assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+    DexInspector dexInspector =
+        isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+    ClassSubject testClass = dexInspector.clazz(TestClass.class);
+    assertThat(testClass, isPresent());
+
+    // Test the totally unused method.
+    MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+    assertThat(staticMethod, isPresent());
+    assertEquals(minify, staticMethod.isRenamed());
+    if (isR8(shrinker)) {
+      assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
+    } else {
+      assertFalse(staticMethod.getMethod().accessFlags.isPublic());
+    }
+
+    // Test an indirectly referred method.
+    staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+    assertThat(staticMethod, isPresent());
+    assertEquals(minify, staticMethod.isRenamed());
+    boolean publicizeCondition = isR8(shrinker) ? allowAccessModification
+        : minify && repackagePrefix != null && allowAccessModification;
+    assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
   }
 
   @Test
   @IgnoreIfVmOlderThan(Version.V7_0_0)
-  public void test() throws Exception {
-    doTest(true, true);
-    doTest(true, false);
-    doTest(false, true);
-    doTest(false, false);
+  public void test_keepAll_R8() throws Exception {
+    doTest_keepAll(Shrinker.R8, "r8", true, true);
+    doTest_keepAll(Shrinker.R8, "r8", true, false);
+    doTest_keepAll(Shrinker.R8, "r8", false, true);
+    doTest_keepAll(Shrinker.R8, "r8", false, false);
+    doTest_keepAll(Shrinker.R8, null, true, true);
+    doTest_keepAll(Shrinker.R8, null, true, false);
+    doTest_keepAll(Shrinker.R8, null, false, true);
+    doTest_keepAll(Shrinker.R8, null, false, false);
+  }
+
+  @Ignore("b/92236970")
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepAll_R8Compat() throws Exception {
+    doTest_keepAll(Shrinker.R8_COMPAT, "rc", true, true);
+    doTest_keepAll(Shrinker.R8_COMPAT, "rc", true, false);
+    doTest_keepAll(Shrinker.R8_COMPAT, "rc", false, true);
+    doTest_keepAll(Shrinker.R8_COMPAT, "rc", false, false);
+    doTest_keepAll(Shrinker.R8_COMPAT, null, true, true);
+    doTest_keepAll(Shrinker.R8_COMPAT, null, true, false);
+    doTest_keepAll(Shrinker.R8_COMPAT, null, false, true);
+    doTest_keepAll(Shrinker.R8_COMPAT, null, false, false);
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepAll_Proguard6() throws Exception {
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+    doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, false, false);
+  }
+
+  private void doTest_keepNonPublic(
+      Shrinker shrinker,
+      String repackagePrefix,
+      boolean allowAccessModification,
+      boolean minify) throws Exception {
+    Class mainClass = TestMain.class;
+    String keep = !minify ? "-keep" : "-keep,allowobfuscation";
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+        allowAccessModification ? "-allowaccessmodification" : "",
+        !minify ? "-dontobfuscate" : "",
+        "-keep class " + mainClass.getCanonicalName() + " {",
+        "  public void main(java.lang.String[]);",
+        "}",
+        keep + " class " + TestClass.class.getCanonicalName() + " {",
+        "  !public <methods>;",
+        "}",
+        keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+        "  !public <methods>;",
+        "}",
+        "-dontwarn java.lang.invoke.*"
+    );
+
+    AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
+    assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+    DexInspector dexInspector =
+        isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+    ClassSubject testClass = dexInspector.clazz(TestClass.class);
+    assertThat(testClass, isPresent());
+
+    // Test the totally unused method.
+    MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+    assertThat(staticMethod, isPresent());
+    assertEquals(minify, staticMethod.isRenamed());
+    if (isR8(shrinker)) {
+      assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
+    } else {
+      assertFalse(staticMethod.getMethod().accessFlags.isPublic());
+    }
+
+    // Test an indirectly referred method.
+    staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+    assertThat(staticMethod, isPresent());
+    assertEquals(minify, staticMethod.isRenamed());
+    boolean publicizeCondition = isR8(shrinker) ? allowAccessModification
+        : minify && repackagePrefix != null && allowAccessModification;
+    assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepNonPublic_R8() throws Exception {
+    doTest_keepNonPublic(Shrinker.R8, "r8", true, true);
+    doTest_keepNonPublic(Shrinker.R8, "r8", true, false);
+    doTest_keepNonPublic(Shrinker.R8, "r8", false, true);
+    doTest_keepNonPublic(Shrinker.R8, "r8", false, false);
+    doTest_keepNonPublic(Shrinker.R8, null, true, true);
+    doTest_keepNonPublic(Shrinker.R8, null, true, false);
+    doTest_keepNonPublic(Shrinker.R8, null, false, true);
+    doTest_keepNonPublic(Shrinker.R8, null, false, false);
+  }
+
+  @Ignore("b/92236970")
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepNonPublic_R8Compat() throws Exception {
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", true, true);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", true, false);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", false, true);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", false, false);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, null, true, true);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, null, true, false);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, null, false, true);
+    doTest_keepNonPublic(Shrinker.R8_COMPAT, null, false, false);
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepNonPublic_Proguard6() throws Exception {
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+    doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, false, false);
+  }
+
+  private void doTest_keepPublic(
+      Shrinker shrinker,
+      String repackagePrefix,
+      boolean allowAccessModification,
+      boolean minify) throws Exception {
+    Class mainClass = TestMain.class;
+    String keep = !minify ? "-keep" : "-keep,allowobfuscation";
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+        allowAccessModification ? "-allowaccessmodification" : "",
+        !minify ? "-dontobfuscate" : "",
+        "-keep class " + mainClass.getCanonicalName() + " {",
+        "  public void main(java.lang.String[]);",
+        "}",
+        keep + " class " + TestClass.class.getCanonicalName() + " {",
+        "  public <methods>;",
+        "}",
+        keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+        "  public <methods>;",
+        "}",
+        "-dontwarn java.lang.invoke.*"
+    );
+
+    AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
+    assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+    DexInspector dexInspector =
+        isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+    ClassSubject testClass = dexInspector.clazz(TestClass.class);
+    assertThat(testClass, isPresent());
+
+    // Test the totally unused method.
+    MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+    assertThat(staticMethod, not(isPresent()));
+
+    // Test an indirectly referred method.
+    staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+    if (isR8(shrinker)) {
+      // Inlined.
+      assertThat(staticMethod, not(isPresent()));
+    } else {
+      assertThat(staticMethod, isPresent());
+      assertEquals(minify, staticMethod.isRenamed());
+      boolean publicizeCondition = minify && repackagePrefix != null && allowAccessModification;
+      assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+    }
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepPublic_R8() throws Exception {
+    doTest_keepPublic(Shrinker.R8, "r8", true, true);
+    doTest_keepPublic(Shrinker.R8, "r8", true, false);
+    doTest_keepPublic(Shrinker.R8, "r8", false, true);
+    doTest_keepPublic(Shrinker.R8, "r8", false, false);
+    doTest_keepPublic(Shrinker.R8, null, true, true);
+    doTest_keepPublic(Shrinker.R8, null, true, false);
+    doTest_keepPublic(Shrinker.R8, null, false, true);
+    doTest_keepPublic(Shrinker.R8, null, false, false);
+  }
+
+  @Ignore("b/92236970")
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepPublic_R8Compat() throws Exception {
+    doTest_keepPublic(Shrinker.R8_COMPAT, "rc", true, true);
+    doTest_keepPublic(Shrinker.R8_COMPAT, "rc", true, false);
+    doTest_keepPublic(Shrinker.R8_COMPAT, "rc", false, true);
+    doTest_keepPublic(Shrinker.R8_COMPAT, "rc", false, false);
+    doTest_keepPublic(Shrinker.R8_COMPAT, null, true, true);
+    doTest_keepPublic(Shrinker.R8_COMPAT, null, true, false);
+    doTest_keepPublic(Shrinker.R8_COMPAT, null, false, true);
+    doTest_keepPublic(Shrinker.R8_COMPAT, null, false, false);
+  }
+
+  @Test
+  @IgnoreIfVmOlderThan(Version.V7_0_0)
+  public void test_keepPublic_Proguard6() throws Exception {
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+    doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, false, false);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java b/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
index 88efc09..f13700c 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
@@ -18,6 +18,10 @@
     return value;
   }
 
+  static void unused() {
+    System.out.println("This should be discarded unless there is a keep rule.");
+  }
+
   static String staticMethod() {
     return "1";
   }
diff --git a/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java b/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
index eb1f245..a9eb622 100644
--- a/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
+++ b/src/test/java/com/android/tools/r8/naming/overloadaggressively/OverloadAggressivelyTest.java
@@ -8,9 +8,9 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.VmTestRunner;
@@ -77,7 +77,7 @@
     assertEquals(overloadaggressively, f2.field.name == f3.field.name);
 
     String main = FieldUpdater.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
     ProcessResult artOutput = runOnArtRaw(processedApp, main);
     // TODO(b/72858955): eventually, R8 should avoid this field resolution conflict.
@@ -123,7 +123,7 @@
     assertEquals(overloadaggressively, f1.field.name == f3.field.name);
 
     String main = FieldResolution.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
     ProcessResult artOutput = runOnArtRaw(processedApp, main);
     // TODO(b/72858955): R8 should avoid field resolution conflict even w/ -overloadaggressively.
@@ -175,7 +175,7 @@
     assertEquals(overloadaggressively, m2.method.name == m3.method.name);
 
     String main = MethodResolution.class.getCanonicalName();
-    ProcessResult javaOutput = runOnJava(main, classes);
+    ProcessResult javaOutput = runOnJavaRaw(main, classes);
     assertEquals(0, javaOutput.exitCode);
     ProcessResult artOutput = runOnArtRaw(processedApp, main);
     // TODO(b/72858955): R8 should avoid method resolution conflict even w/ -overloadaggressively.
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
index 8a6bdcf..bfb0674 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
@@ -231,21 +231,25 @@
       mv.visitJumpInsn(IFEQ, l7_b1);
       mv.visitLabel(l7_b0);
       mv.visitInsn(ACONST_NULL);
+      // At this point, NULL flows to l7_join.
       mv.visitJumpInsn(GOTO, l7_join);
       mv.visitLabel(l7_b1);
       mv.visitVarInsn(ALOAD, 10);
-      // Swap the new-instance or null, with the byte-array.
+      // At this point, an uninitialized String flows to l7_join.
       mv.visitLabel(l7_join);
+      // At this point, stack contains [staticByteArray, NULL/uninitialized]
       mv.visitInsn(SWAP);
-      // Load the new-instacne and swap again with the byte-array.
+      // At this point, stack contains [NULL/uninitialized, staticByteArray]
       mv.visitVarInsn(ALOAD, 10);
+      // At this point, stack contains [NULL/uninitialized, staticByteArray, uninitialized]
       mv.visitInsn(SWAP);
+      // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray]
       mv.visitInsn(ICONST_0);
-      // Invoke special will now always be on the new-instance as receiver.
+      // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray, 0]
       mv.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "<init>", "([BI)V", false);
-      // Return will be either new-instance or null.
+      // Just before ARETURN, stack contains [NULL/String].
       // This will force a non-trivial phi of the new-instance on this block, ie, prior to <init>.
-      // This phi must remain.
+      // This phi must remain since it is not trivial.
       mv.visitInsn(ARETURN);
       mv.visitLabel(l8);
       mv.visitVarInsn(ILOAD, 0);
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
index ff7da8f..ccf6467 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
@@ -31,13 +31,14 @@
 
   public static void compare(String output, int iterations) {
     String expected = "java.security.SecureRandom";
-    if (output.equals(expected)) {
+    if (expected.equals(output)) {
       return;
     }
     System.out.println(
         "After " + iterations + " iterations, expected \"" +
         expected + "\", but got \"" + output + "\"");
-    System.exit(1);
+    // Exit with code 0 to allow test to use ensureSameOutput().
+    System.exit(0);
   }
 
   public static void compareHash(int a, int b, byte[] c, int iterations) {
@@ -53,6 +54,7 @@
     System.out.println("staticIntB: " + b);
     System.out.print("staticIntByteArray: ");
     printByteArray(c);
-    System.exit(1);
+    // Exit with code 0 to allow test to use ensureSameOutput().
+    System.exit(0);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
index 3c7c465..ee37896 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
@@ -3,8 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.regress.b78493232;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.AndroidApp;
 import org.junit.Test;
 
 // Variant of Regress78493232, but where the new-instance is forced to flow to a non-trivial phi
@@ -15,6 +22,57 @@
   public void test() throws Exception {
     // Run test on JVM and ART(x86) to ensure expected behavior.
     // Running the same test on an ARM JIT causes errors.
+
+    // TODO(b/80118070): Remove this if-statement when fixed.
+    if (ToolHelper.getDexVm().getVersion() != Version.V5_1_1) {
+      AndroidApp app =
+          buildAndroidApp(
+              Regress78493232Dump_WithPhi.dump(),
+              ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+      ProcessResult javaResult =
+          runOnJavaRaw(
+              Regress78493232Dump_WithPhi.CLASS_NAME,
+              Regress78493232Dump_WithPhi.dump(),
+              ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+      ProcessResult d8Result =
+          runOnArtRaw(compileWithD8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+      ProcessResult r8Result =
+          runOnArtRaw(compileWithR8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+      String proguardConfig =
+          keepMainProguardConfiguration(Regress78493232Dump_WithPhi.CLASS_NAME)
+              + "-dontobfuscate\n";
+      ProcessResult r8ShakenResult =
+          runOnArtRaw(compileWithR8(app, proguardConfig), Regress78493232Dump_WithPhi.CLASS_NAME);
+      assertEquals(
+          "After 0 iterations, expected \"java.security.SecureRandom\", but got \"null\"\n",
+          javaResult.stdout);
+      assertEquals(0, javaResult.exitCode);
+      switch (ToolHelper.getDexVm().getVersion()) {
+        case V4_0_4:
+        case V4_4_4:
+        case V7_0_0:
+        case DEFAULT:
+          assertNotEquals(-1, d8Result.stderr.indexOf("java.lang.VerifyError"));
+          assertNotEquals(-1, r8Result.stderr.indexOf("java.lang.VerifyError"));
+          assertNotEquals(-1, r8ShakenResult.stderr.indexOf("java.lang.VerifyError"));
+          assertEquals(1, d8Result.exitCode);
+          assertEquals(1, r8Result.exitCode);
+          assertEquals(1, r8ShakenResult.exitCode);
+          break;
+        case V6_0_1:
+          assertEquals("Completed successfully after 1000 iterations\n", d8Result.stdout);
+          assertEquals("Completed successfully after 1000 iterations\n", r8Result.stdout);
+          assertEquals("Completed successfully after 1000 iterations\n", r8ShakenResult.stdout);
+          assertEquals(0, d8Result.exitCode);
+          assertEquals(0, r8Result.exitCode);
+          assertEquals(0, r8ShakenResult.exitCode);
+          break;
+        case V5_1_1:
+        default:
+          throw new Unreachable();
+      }
+      return;
+    }
     ensureSameOutput(
         Regress78493232Dump_WithPhi.CLASS_NAME,
         Regress78493232Dump_WithPhi.dump(),
diff --git a/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java b/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
index f27813e..7c243ae 100644
--- a/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
@@ -34,16 +34,8 @@
 
   private void testPath(List<String> filters, List<String> positives, List<String> negatives) {
     FilteredClassPath path = makeFilteredClassPath(filters);
-    Assert.assertTrue(
-        positives.stream().map(FilteredClassPathTest::adaptFileSeparator).map(Paths::get)
-            .allMatch(path::matchesFile));
-    Assert.assertFalse(
-        negatives.stream().map(FilteredClassPathTest::adaptFileSeparator).map(Paths::get)
-            .allMatch(path::matchesFile));
-  }
-
-  private static String adaptFileSeparator(String s) {
-    return s.replace('/', File.separatorChar);
+    Assert.assertTrue(positives.stream().allMatch(path::matchesFile));
+    Assert.assertFalse(negatives.stream().allMatch(path::matchesFile));
   }
 
   private static FilteredClassPath makeFilteredClassPath(List<String> filters) {
@@ -51,9 +43,7 @@
   }
 
   private static FilteredClassPath makeFilteredClassPath(Path path, List<String> filters) {
-    return new FilteredClassPath(path,
-        filters.stream().map(FilteredClassPathTest::adaptFileSeparator)
-            .collect(ImmutableList.toImmutableList()));
+    return new FilteredClassPath(path, ImmutableList.copyOf(filters));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
index 3976f89..10c64e3 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
@@ -6,24 +6,31 @@
 import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompatProguardCommandBuilder;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.io.File;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class ProguardCompatabilityTestBase extends TestBase {
 
+  protected Path proguardMap;
+
   public enum Shrinker {
     PROGUARD5,
     PROGUARD6,
@@ -32,6 +39,34 @@
     R8
   }
 
+  protected static boolean isR8(Shrinker shrinker) {
+    return shrinker == Shrinker.R8_COMPAT || shrinker == Shrinker.R8;
+  }
+
+  protected AndroidApp runShrinkerRaw(
+      Shrinker mode, List<Class> programClasses, List<String> proguadConfigs) throws Exception {
+    return runShrinkerRaw(
+        mode, programClasses, String.join(System.lineSeparator(), proguadConfigs));
+  }
+
+  protected AndroidApp runShrinkerRaw(
+      Shrinker mode, List<Class> programClasses, String proguardConfig) throws Exception {
+    proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+    switch (mode) {
+      case PROGUARD5:
+        return runProguard5Raw(programClasses, proguardConfig, proguardMap);
+      case PROGUARD6:
+        return runProguard6Raw(programClasses, proguardConfig, proguardMap);
+      case PROGUARD6_THEN_D8:
+        return runProguard6AndD8Raw(programClasses, proguardConfig, proguardMap);
+      case R8_COMPAT:
+        return runR8CompatRaw(programClasses, proguardConfig);
+      case R8:
+        return runR8Raw(programClasses, proguardConfig);
+    }
+    throw new IllegalArgumentException("Unknown shrinker: " + mode);
+  }
+
   protected DexInspector runShrinker(
       Shrinker mode, List<Class> programClasses, List<String> proguadConfigs) throws Exception {
     return runShrinker(mode, programClasses, String.join(System.lineSeparator(), proguadConfigs));
@@ -54,21 +89,38 @@
     throw new IllegalArgumentException("Unknown shrinker: " + mode);
   }
 
-  protected DexInspector runR8(List<Class> programClasses, String proguardConfig) throws Exception {
-    AndroidApp app = readClasses(programClasses);
-    R8Command.Builder builder = ToolHelper.prepareR8CommandBuilder(app);
-    builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
-    return new DexInspector(ToolHelper.runR8(builder.build()));
+  protected AndroidApp runR8Raw(List<Class> programClasses, String proguardConfig)
+      throws Exception {
+    return runR8Raw(programClasses, proguardConfig, null);
   }
 
-  protected DexInspector runR8Compat(
+  protected AndroidApp runR8Raw(
+      List<Class> programClasses, String proguardConfig, Consumer<InternalOptions> configure)
+      throws Exception {
+    AndroidApp app = readClassesAndAndriodJar(programClasses);
+    R8Command.Builder builder = ToolHelper.prepareR8CommandBuilder(app);
+    builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
+    return ToolHelper.runR8(builder.build(), configure);
+  }
+
+  protected DexInspector runR8(List<Class> programClasses, String proguardConfig) throws Exception {
+    return new DexInspector(runR8Raw(programClasses, proguardConfig));
+  }
+
+  protected AndroidApp runR8CompatRaw(
       List<Class> programClasses, String proguardConfig) throws Exception {
     CompatProguardCommandBuilder builder = new CompatProguardCommandBuilder(true);
     builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
     programClasses.forEach(
         clazz -> builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz)));
+    builder.addLibraryFiles(ToolHelper.getDefaultAndroidJar());
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    return new DexInspector(ToolHelper.runR8(builder.build()));
+    return ToolHelper.runR8(builder.build());
+  }
+
+  protected DexInspector runR8Compat(
+      List<Class> programClasses, String proguardConfig) throws Exception {
+    return new DexInspector(runR8CompatRaw(programClasses, proguardConfig));
   }
 
   protected DexInspector runR8CompatKeepingMain(Class mainClass, List<Class> programClasses)
@@ -76,41 +128,79 @@
     return runR8Compat(programClasses, keepMainProguardConfiguration(mainClass));
   }
 
-  protected DexInspector runProguard5(
-      List<Class> programClasses, String proguardConfig) throws Exception {
+  protected AndroidApp runProguard5Raw(
+      List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
     Path proguardedJar =
         File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
     Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
-    Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
     FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
-    ToolHelper.runProguard(
-        jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
-    return new DexInspector(readJar(proguardedJar), proguardMap);
+    ProcessResult result = ToolHelper.runProguardRaw(
+        jarTestClasses(programClasses),
+        proguardedJar,
+        ToolHelper.getAndroidJar(AndroidApiLevel.N),
+        proguardConfigFile,
+        proguardMap);
+    if (result.exitCode != 0) {
+      fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+    }
+    return readJar(proguardedJar);
+  }
+
+  protected DexInspector runProguard5(
+      List<Class> programClasses, String proguardConfig) throws Exception {
+    proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+    return new DexInspector(
+        runProguard5Raw(programClasses, proguardConfig, proguardMap), proguardMap);
+  }
+
+  protected AndroidApp runProguard6Raw(
+      List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
+    Path proguardedJar =
+        File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
+    Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
+    FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
+    ProcessResult result = ToolHelper.runProguard6Raw(
+        jarTestClasses(programClasses),
+        proguardedJar,
+        ToolHelper.getAndroidJar(AndroidApiLevel.N),
+        proguardConfigFile,
+        proguardMap);
+    if (result.exitCode != 0) {
+      fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+    }
+    return readJar(proguardedJar);
   }
 
   protected DexInspector runProguard6(
       List<Class> programClasses, String proguardConfig) throws Exception {
+    proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+    return new DexInspector(
+        runProguard6Raw(programClasses, proguardConfig, proguardMap), proguardMap);
+  }
+
+  protected AndroidApp runProguard6AndD8Raw(
+      List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
     Path proguardedJar =
         File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
     Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
-    Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
     FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
-    ToolHelper.runProguard6(
-        jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
-    return new DexInspector(readJar(proguardedJar), proguardMap);
+    ProcessResult result = ToolHelper.runProguard6Raw(
+        jarTestClasses(programClasses),
+        proguardedJar,
+        ToolHelper.getAndroidJar(AndroidApiLevel.N),
+        proguardConfigFile,
+        proguardMap);
+    if (result.exitCode != 0) {
+      fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+    }
+    return ToolHelper.runD8(readJar(proguardedJar));
   }
 
   protected DexInspector runProguard6AndD8(
       List<Class> programClasses, String proguardConfig) throws Exception {
-    Path proguardedJar =
-        File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
-    Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
-    Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
-    FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
-    ToolHelper.runProguard6(
-        jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
-    AndroidApp app = ToolHelper.runD8(readJar(proguardedJar));
-    return new DexInspector(app, proguardMap);
+    proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+    return new DexInspector(
+        runProguard6AndD8Raw(programClasses, proguardConfig, proguardMap), proguardMap);
   }
 
   protected DexInspector runProguardKeepingMain(Class mainClass, List<Class> programClasses)
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
new file mode 100644
index 0000000..65d1763
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
@@ -0,0 +1,195 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking.ifrule;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+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 IfOnAccessModifierTest extends ProguardCompatabilityTestBase {
+    private final static List<Class> CLASSES = ImmutableList.of(
+        ClassForIf.class, ClassForSubsequent.class,
+        MainForAccessModifierTest.class);
+
+  private final Shrinker shrinker;
+  private final MethodSignature nonPublicMethod;
+  private final MethodSignature publicMethod;
+
+  public IfOnAccessModifierTest(Shrinker shrinker) {
+    this.shrinker = shrinker;
+    nonPublicMethod = new MethodSignature("nonPublicMethod", "void", ImmutableList.of());
+    publicMethod = new MethodSignature("publicMethod", "void", ImmutableList.of());
+  }
+
+  @Parameters(name = "shrinker: {0}")
+  public static Collection<Object> data() {
+    return ImmutableList.of(Shrinker.PROGUARD6, Shrinker.R8);
+  }
+
+  @Test
+  public void ifOnNonPublic_keepOnPublic() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        "-repackageclasses 'top'",
+        "-allowaccessmodification",
+        "-keep class **.Main* {",
+        "  public static void callIfNonPublic();",
+        "}",
+        "-if class **.ClassForIf {",
+        "  !public <methods>;",
+        "}",
+        "-keep,allowobfuscation class **.ClassForSubsequent {",
+        "  public <methods>;",
+        "}"
+    );
+    DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+    ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, not(isPresent()));
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getMethod().accessFlags.isPublic());
+
+    classSubject = dexInspector.clazz(ClassForSubsequent.class);
+    if (isR8(shrinker)) {
+      // TODO(b/72109068): ClassForIf#nonPublicMethod becomes public, and -if rule is not applied
+      // at the 2nd tree shaking.
+      assertThat(classSubject, not(isPresent()));
+      return;
+    }
+    assertThat(classSubject, isPresent());
+    methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, isPresent());
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, not(isPresent()));
+  }
+
+  @Test
+  public void ifOnNonPublic_keepOnNonPublic() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        "-repackageclasses 'top'",
+        "-allowaccessmodification",
+        "-keep class **.Main* {",
+        "  public static void callIfNonPublic();",
+        "}",
+        "-if class **.ClassForIf {",
+        "  !public <methods>;",
+        "}",
+        "-keep,allowobfuscation class **.ClassForSubsequent {",
+        "  !public <methods>;",
+        "}"
+    );
+    DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+    ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, not(isPresent()));
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getMethod().accessFlags.isPublic());
+
+    classSubject = dexInspector.clazz(ClassForSubsequent.class);
+    if (isR8(shrinker)) {
+      // TODO(b/72109068): ClassForIf#nonPublicMethod becomes public, and -if rule is not applied
+      // at the 2nd tree shaking.
+      assertThat(classSubject, not(isPresent()));
+      return;
+    }
+    assertThat(classSubject, isPresent());
+    methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, not(isPresent()));
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, isPresent());
+    assertFalse(methodSubject.getMethod().accessFlags.isPublic());
+  }
+
+  @Test
+  public void ifOnPublic_keepOnPublic() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        "-repackageclasses 'top'",
+        "-allowaccessmodification",
+        "-keep class **.Main* {",
+        "  public static void callIfPublic();",
+        "}",
+        "-if class **.ClassForIf {",
+        "  public <methods>;",
+        "}",
+        "-keep,allowobfuscation class **.ClassForSubsequent {",
+        "  public <methods>;",
+        "}"
+    );
+    DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+    ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, isPresent());
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, not(isPresent()));
+
+    classSubject = dexInspector.clazz(ClassForSubsequent.class);
+    assertThat(classSubject, isPresent());
+    methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, isPresent());
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, not(isPresent()));
+  }
+
+  @Test
+  public void ifOnPublic_keepOnNonPublic() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        "-repackageclasses 'top'",
+        "-allowaccessmodification",
+        "-keep class **.Main* {",
+        "  public static void callIfPublic();",
+        "}",
+        "-if class **.ClassForIf {",
+        "  public <methods>;",
+        "}",
+        "-keep,allowobfuscation class **.ClassForSubsequent {",
+        "  !public <methods>;",
+        "}"
+    );
+    DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+    ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+    assertThat(classSubject, isPresent());
+    MethodSubject methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, isPresent());
+    methodSubject = classSubject.method(nonPublicMethod);
+    assertThat(methodSubject, not(isPresent()));
+
+    classSubject = dexInspector.clazz(ClassForSubsequent.class);
+    assertThat(classSubject, isPresent());
+    methodSubject = classSubject.method(publicMethod);
+    assertThat(methodSubject, not(isPresent()));
+    methodSubject = classSubject.method(nonPublicMethod);
+    if (isR8(shrinker)) {
+      // TODO(b/72109068): if kept in the 1st tree shaking, should be kept after publicizing.
+      assertThat(methodSubject, not(isPresent()));
+      return;
+    }
+    assertThat(methodSubject, isPresent());
+    assertFalse(methodSubject.getMethod().accessFlags.isPublic());
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java
new file mode 100644
index 0000000..1b5bd75
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking.ifrule;
+
+class ClassForIf {
+  ClassForIf() {
+  }
+
+  synchronized void nonPublicMethod() {
+    System.out.println("ClassForIf::nonPublicMethod");
+  }
+
+  synchronized public void publicMethod() {
+    System.out.println("ClassForIf::publicMethod");
+  }
+}
+
+class ClassForSubsequent {
+  ClassForSubsequent() {
+  }
+
+  synchronized void nonPublicMethod() {
+    System.out.println("ClassForSubsequent::nonPublicMethod");
+  }
+
+  synchronized public void publicMethod() {
+    System.out.println("ClassForSubsequent::publicMethod");
+  }
+}
+
+class MainForAccessModifierTest {
+  public static void callIfNonPublic() {
+    new ClassForIf().nonPublicMethod();
+  }
+
+  public static void callIfPublic() {
+    new ClassForIf().publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
new file mode 100644
index 0000000..14391ee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.ifrule.inlining;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+class A {
+  static public int a() {
+    try {
+      String p = A.class.getPackage().getName();
+      Class.forName(p + ".D");
+    } catch (ClassNotFoundException e) {
+      return 2;
+    }
+    return 1;
+  }
+}
+
+class B {
+  // Depending on inlining option, this method is kept to make inlining of A.a() infeasible.
+  void x() {
+    System.out.print("" + A.a() + A.a() + A.a() + A.a() + A.a() + A.a() + A.a() + A.a());
+  }
+}
+
+class D {
+}
+
+class Main {
+  public static void main(String[] args) {
+    System.out.print("" + A.a());
+  }
+}
+
+@RunWith(Parameterized.class)
+public class IfRuleWithInlining extends ProguardCompatabilityTestBase {
+  private final static List<Class> CLASSES = ImmutableList.of(
+      A.class, B.class, D.class, Main.class);
+
+  private final Shrinker shrinker;
+  private final boolean inlineMethod;
+
+  public IfRuleWithInlining(Shrinker shrinker, boolean inlineMethod) {
+    this.shrinker = shrinker;
+    this.inlineMethod = inlineMethod;
+  }
+
+  @Parameters(name = "shrinker: {0} inlineMethod: {1}")
+  public static Collection<Object[]> data() {
+    // We don't run this on Proguard, as triggering inlining in Proguard is out of our control.
+    return ImmutableList.of(
+        new Object[]{Shrinker.R8, true},
+        new Object[]{Shrinker.R8, false}
+    );
+  }
+
+  private void check(AndroidApp app) throws Exception {
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazzA = inspector.clazz(A.class);
+    assertThat(clazzA, isPresent());
+    // A.a might be inlined.
+    assertEquals(!inlineMethod, clazzA.method("int", "a", ImmutableList.of()).isPresent());
+    // TODO(110148109): class D should be present - inlining or not.
+    assertEquals(!inlineMethod, inspector.clazz(D.class).isPresent());
+    ProcessResult result = runOnArtRaw(app, Main.class.getName());
+    assertEquals(0, result.exitCode);
+    // TODO(110148109): Output should be the same - inlining or not.
+    assertEquals(!inlineMethod ? "1" : "2", result.stdout);
+  }
+
+  @Test
+  public void testMergedClassMethodInIfRule() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-keep class **.Main { public static void main(java.lang.String[]); }",
+        inlineMethod
+            ? "-alwaysinline class **.A { int a(); }"
+            : "-keep class **.B { *; }",
+        inlineMethod
+            ? "-checkdiscard class **.A { int a(); }"
+            : "",
+        "-if class **.A { static int a(); }",
+        "-keep class **.D",
+        "-dontobfuscate"
+    );
+
+    check(runShrinkerRaw(shrinker, CLASSES, config));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
new file mode 100644
index 0000000..b484d73
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
@@ -0,0 +1,156 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking.ifrule.verticalclassmerging;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+class A {
+  int x = 1;
+  int a() throws ClassNotFoundException {
+    // Class D is expected to be kept - vertical class merging or not. The -if rule say that if
+    // the method A.a is in the output, then class D is needed.
+    String p =getClass().getPackage().getName();
+    Class.forName(p + ".D");
+    return 4;
+  }
+}
+
+class B extends A {
+  int y = 2;
+  int b() {
+    return 5;
+  }
+}
+
+class C extends B {
+  int z = 3;
+  int c() {
+    return 6;
+  }
+}
+
+class D {
+}
+
+class Main {
+  public static void main(String[] args) throws ClassNotFoundException {
+    C c = new C();
+    System.out.print("" + c.x + "" + c.y + "" + c.z + "" + c.a() + "" + c.b() + "" + c.c());
+  }
+}
+
+@RunWith(Parameterized.class)
+public class IfRuleWithVerticalClassMerging extends ProguardCompatabilityTestBase {
+  private final static List<Class> CLASSES = ImmutableList.of(
+      A.class, B.class, C.class, D.class, Main.class);
+
+  private final Shrinker shrinker;
+  private final boolean enableClassMerging;
+
+  public IfRuleWithVerticalClassMerging(Shrinker shrinker, boolean enableClassMerging) {
+    this.shrinker = shrinker;
+    this.enableClassMerging = enableClassMerging;
+  }
+
+  @Parameters(name = "shrinker: {0} classMerging: {1}")
+  public static Collection<Object[]> data() {
+    // We don't run this on Proguard, as Proguard does not merge A into B.
+    return ImmutableList.of(
+        new Object[]{Shrinker.R8, true},
+        new Object[]{Shrinker.R8, false}
+    );
+  }
+
+  private void configure(InternalOptions options) {
+    options.enableClassMerging = enableClassMerging;
+  }
+
+  @Override
+  protected AndroidApp runR8Raw(
+      List<Class> programClasses, String proguardConfig) throws Exception {
+    return super.runR8Raw(programClasses, proguardConfig, this::configure);
+  }
+
+  private void check(AndroidApp app) throws Exception {
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazzA = inspector.clazz(A.class);
+    assertEquals(!enableClassMerging, clazzA.isPresent());
+    ClassSubject clazzB = inspector.clazz(B.class);
+    assertThat(clazzB, isPresent());
+    ClassSubject clazzD = inspector.clazz(D.class);
+    // TODO(110141157): Class D should be kept - vertical class merging or not.
+    assertEquals(!enableClassMerging, clazzD.isPresent());
+
+    ProcessResult result = runOnArtRaw(app, Main.class.getName());
+    // TODO(110141157): The code should run - vertical class merging or not.
+    assertEquals(enableClassMerging ? 1 : 0, result.exitCode);
+    if (!enableClassMerging) {
+      assertEquals("123456", result.stdout);
+    }
+  }
+
+  @Test
+  public void testMergedClassInIfRule() throws Exception {
+    // Class C is kept, meaning that it will not be touched.
+    // Class A will be merged into class B.
+    List<String> config = ImmutableList.of(
+        "-keep class **.Main { public static void main(java.lang.String[]); }",
+        "-keep class **.C",
+        "-if class **.A",
+        "-keep class **.D",
+        "-dontobfuscate"
+    );
+
+    check(runShrinkerRaw(shrinker, CLASSES, config));
+  }
+
+  @Test
+  public void testMergedClassFieldInIfRule() throws Exception {
+    // Class C is kept, meaning that it will not be touched.
+    // Class A will be merged into class B.
+    // Main.main access A.x, so that field exists satisfying the if rule.
+    List<String> config = ImmutableList.of(
+        "-keep class **.Main { public static void main(java.lang.String[]); }",
+        "-keep class **.C",
+        "-if class **.A { int x; }",
+        "-keep class **.D",
+        "-dontobfuscate"
+    );
+
+    check(runShrinkerRaw(shrinker, CLASSES, config));
+  }
+
+  @Test
+  public void testMergedClassMethodInIfRule() throws Exception {
+    // Class C is kept, meaning that it will not be touched.
+    // Class A will be merged into class B.
+    // Main.main access A.a(), that method exists satisfying the if rule.
+    List<String> config = ImmutableList.of(
+        "-keep class **.Main { public static void main(java.lang.String[]); }",
+        "-keep class **.C",
+        "-if class **.A { int a(); }",
+        "-keep class **.D",
+        "-dontobfuscate"
+    );
+
+    check(runShrinkerRaw(shrinker, CLASSES, config));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java b/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
index 58180e3..7ad9163 100644
--- a/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
+++ b/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
@@ -6,7 +6,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.Test;
 
 public class DescriptorUtilsTest {
@@ -67,4 +70,16 @@
     assertEquals("java.lang.Object", DescriptorUtils.descriptorToJavaType("Ljava/lang/Object;"));
     assertEquals("a.b.C", DescriptorUtils.descriptorToJavaType("La/b/C;"));
   }
+
+  @Test
+  public void guessClassDescriptor() {
+    String obj = "java/lang/Object.class";
+    assertEquals("Ljava/lang/Object;", DescriptorUtils.guessTypeDescriptor(obj));
+    String objBackslash = "java\\lang\\Object.class";
+    assertEquals("Ljava\\lang\\Object;", DescriptorUtils.guessTypeDescriptor(objBackslash));
+    String objFileSeparatorChar =
+        "java" + File.separatorChar + "lang" + File.separatorChar + "Object.class";
+    assertEquals("Ljava/lang/Object;",
+        DescriptorUtils.guessTypeDescriptor(Paths.get(objFileSeparatorChar)));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index ace5c9b..fd32ec7 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.StringResource;
 import com.android.tools.r8.code.Const4;
 import com.android.tools.r8.code.ConstString;
@@ -53,6 +55,8 @@
 import com.android.tools.r8.code.Throw;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexCode;
@@ -65,6 +69,8 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
@@ -72,6 +78,8 @@
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.naming.signature.GenericSignatureAction;
+import com.android.tools.r8.naming.signature.GenericSignatureParser;
 import com.android.tools.r8.smali.SmaliBuilder;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableList;
@@ -85,6 +93,7 @@
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.concurrent.ExecutionException;
+import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -191,6 +200,50 @@
     }
   }
 
+  DexAnnotation findAnnotation(String name, DexAnnotationSet annotations) {
+    for (DexAnnotation annotation : annotations.annotations) {
+      DexType type = annotation.annotation.type;
+      String original = mapping == null ? type.toSourceString() : mapping.originalNameOf(type);
+      if (original.equals(name)) {
+        return annotation;
+      }
+    }
+    return null;
+  }
+
+  public String getFinalSignatureAttribute(DexAnnotationSet annotations) {
+    DexAnnotation annotation =
+        findAnnotation("dalvik.annotation.Signature", annotations);
+    if (annotation == null) {
+      return null;
+    }
+    assert annotation.annotation.elements.length == 1;
+    DexAnnotationElement element = annotation.annotation.elements[0];
+    assert element.value instanceof DexValueArray;
+    StringBuilder builder = new StringBuilder();
+    DexValueArray valueArray = (DexValueArray) element.value;
+    for (DexValue value : valueArray.getValues()) {
+      assertTrue(value instanceof DexValueString);
+      DexValueString s = (DexValueString) value;
+      builder.append(s.getValue());
+    }
+    return builder.toString();
+  }
+
+  public String getOriginalSignatureAttribute(
+      DexAnnotationSet annotations, BiConsumer<GenericSignatureParser, String> parse) {
+    String finalSignature = getFinalSignatureAttribute(annotations);
+    if (finalSignature == null || mapping == null) {
+      return finalSignature;
+    }
+
+    GenericSignatureGenerater rewriter = new GenericSignatureGenerater();
+    GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+    parse.accept(parser, finalSignature);
+    return rewriter.getSignature();
+  }
+
+
   public ClassSubject clazz(Class clazz) {
     return clazz(clazz.getTypeName());
   }
@@ -367,6 +420,10 @@
     public abstract boolean isLocalClass();
 
     public abstract boolean isAnonymousClass();
+
+    public abstract String getOriginalSignatureAttribute();
+
+    public abstract String getFinalSignatureAttribute();
   }
 
   private class AbsentClassSubject extends ClassSubject {
@@ -448,6 +505,16 @@
     public boolean isAnonymousClass() {
       return false;
     }
+
+    @Override
+    public String getOriginalSignatureAttribute() {
+      return null;
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return null;
+    }
   }
 
   public class FoundClassSubject extends ClassSubject {
@@ -561,23 +628,12 @@
       assert !name.endsWith("EnclosingClass")
           && !name.endsWith("EnclosingMethod")
           && !name.endsWith("InnerClass");
-      DexAnnotation annotation = findAnnotation(name);
+      DexAnnotation annotation = findAnnotation(name, dexClass.annotations);
       return annotation == null
           ? new AbsentAnnotationSubject()
           : new FoundAnnotationSubject(annotation);
     }
 
-    private DexAnnotation findAnnotation(String name) {
-      for (DexAnnotation annotation : dexClass.annotations.annotations) {
-        DexType type = annotation.annotation.type;
-        String original = mapping == null ? type.toSourceString() : mapping.originalNameOf(type);
-        if (original.equals(name)) {
-          return annotation;
-        }
-      }
-      return null;
-    }
-
     @Override
     public String getOriginalName() {
       if (naming != null) {
@@ -641,6 +697,17 @@
     }
 
     @Override
+    public String getOriginalSignatureAttribute() {
+      return DexInspector.this.getOriginalSignatureAttribute(
+          dexClass.annotations, GenericSignatureParser::parseClassSignature);
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return DexInspector.this.getFinalSignatureAttribute(dexClass.annotations);
+    }
+
+    @Override
     public String toString() {
       return dexClass.toSourceString();
     }
@@ -677,6 +744,10 @@
 
     public abstract boolean isClassInitializer();
 
+    public abstract String getOriginalSignatureAttribute();
+
+    public abstract String getFinalSignatureAttribute();
+
     public abstract DexEncodedMethod getMethod();
 
     public Iterator<InstructionSubject> iterateInstructions() {
@@ -745,6 +816,16 @@
     public Signature getFinalSignature() {
       return null;
     }
+
+    @Override
+    public String getOriginalSignatureAttribute() {
+      return null;
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return null;
+    }
   }
 
   public class FoundMethodSubject extends MethodSubject {
@@ -805,7 +886,31 @@
     @Override
     public MethodSignature getOriginalSignature() {
       MethodSignature signature = getFinalSignature();
-      MemberNaming memberNaming = clazz.naming != null ? clazz.naming.lookup(signature) : null;
+      if (clazz.naming == null) {
+        return signature;
+      }
+
+      // Map the parameters and return type to original names. This is needed as the in the
+      // Proguard map the names on the left side are the original names. E.g.
+      //
+      //   X -> a
+      //     X method(X) -> a
+      //
+      // whereas the final signature is for X.a is "a (a)"
+      String[] OriginalParameters = new String[signature.parameters.length];
+      for (int i = 0; i < OriginalParameters.length; i++) {
+        String obfuscated = signature.parameters[i];
+        String original = originalToObfuscatedMapping.inverse().get(obfuscated);
+        OriginalParameters[i] = original != null ? original : obfuscated;
+      }
+      String obfuscatedReturnType = signature.type;
+      String originalReturnType = originalToObfuscatedMapping.inverse().get(obfuscatedReturnType);
+      String returnType = originalReturnType != null ? originalReturnType : obfuscatedReturnType;
+
+      MethodSignature lookupSignature =
+          new MethodSignature(signature.name, returnType, OriginalParameters);
+
+      MemberNaming memberNaming = clazz.naming.lookup(lookupSignature);
       return memberNaming != null
           ? (MethodSignature) memberNaming.getOriginalSignature()
           : signature;
@@ -817,6 +922,17 @@
     }
 
     @Override
+    public String getOriginalSignatureAttribute() {
+      return DexInspector.this.getOriginalSignatureAttribute(
+          dexMethod.annotations, GenericSignatureParser::parseMethodSignature);
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return DexInspector.this.getFinalSignatureAttribute(dexMethod.annotations);
+    }
+
+    @Override
     public Iterator<InstructionSubject> iterateInstructions() {
       return new InstructionIterator(this);
     }
@@ -841,6 +957,10 @@
     public abstract DexValue getStaticValue();
 
     public abstract boolean isRenamed();
+
+    public abstract String getOriginalSignatureAttribute();
+
+    public abstract String getFinalSignatureAttribute();
   }
 
   public class AbsentFieldSubject extends FieldSubject {
@@ -889,6 +1009,16 @@
     public DexEncodedField getField() {
       return null;
     }
+
+    @Override
+    public String getOriginalSignatureAttribute() {
+      return null;
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return null;
+    }
   }
 
   public class FoundFieldSubject extends FieldSubject {
@@ -929,7 +1059,24 @@
     @Override
     public FieldSignature getOriginalSignature() {
       FieldSignature signature = getFinalSignature();
-      MemberNaming memberNaming = clazz.naming != null ? clazz.naming.lookup(signature) : null;
+      if (clazz.naming == null) {
+        return signature;
+      }
+
+      // Map the type to the original name. This is needed as the in the Proguard map the
+      // names on the left side are the original names. E.g.
+      //
+      //   X -> a
+      //     X field -> a
+      //
+      // whereas the final signature is for X.a is "a a"
+      String obfuscatedType = signature.type;
+      String originalType = originalToObfuscatedMapping.inverse().get(obfuscatedType);
+      String fieldType = originalType != null ? originalType : obfuscatedType;
+
+      FieldSignature lookupSignature = new FieldSignature(signature.name, fieldType);
+
+      MemberNaming memberNaming = clazz.naming.lookup(lookupSignature);
       return memberNaming != null
           ? (FieldSignature) memberNaming.getOriginalSignature()
           : signature;
@@ -956,6 +1103,17 @@
     }
 
     @Override
+    public String getOriginalSignatureAttribute() {
+      return DexInspector.this.getOriginalSignatureAttribute(
+          dexField.annotations, GenericSignatureParser::parseFieldSignature);
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return DexInspector.this.getFinalSignatureAttribute(dexField.annotations);
+    }
+
+    @Override
     public String toString() {
       return dexField.toSourceString();
     }
@@ -1410,4 +1568,63 @@
       return result;
     }
   }
+
+  // Build the generic signature using the current mapping if any.
+  class GenericSignatureGenerater implements GenericSignatureAction<String> {
+
+    private StringBuilder signature;
+
+    public String getSignature() {
+      return signature.toString();
+    }
+
+    @Override
+    public void parsedSymbol(char symbol) {
+      signature.append(symbol);
+    }
+
+    @Override
+    public void parsedIdentifier(String identifier) {
+      signature.append(identifier);
+    }
+
+    @Override
+    public String parsedTypeName(String name) {
+      String type = name;
+      if (originalToObfuscatedMapping != null) {
+        String original = originalToObfuscatedMapping.inverse().get(name);
+        type = original != null ? original : name;
+      }
+      signature.append(type);
+      return type;
+    }
+
+    @Override
+    public String parsedInnerTypeName(String enclosingType, String name) {
+      String type;
+      if (originalToObfuscatedMapping != null) {
+        // The enclosingType has already been mapped if a mapping is present.
+        String minifiedEnclosing = originalToObfuscatedMapping.get(enclosingType);
+        type = originalToObfuscatedMapping.inverse().get(minifiedEnclosing + "$" + name);
+        if (type != null) {
+          assert type.startsWith(enclosingType + "$");
+          name = type.substring(enclosingType.length() + 1);
+        }
+      } else {
+        type = enclosingType + "$" + name;
+      }
+      signature.append(name);
+      return type;
+    }
+
+    @Override
+    public void start() {
+      signature = new StringBuilder();
+    }
+
+    @Override
+    public void stop() {
+      // nothing to do
+    }
+  }
 }
diff --git a/tools/api_sample_coverage.py b/tools/api_sample_coverage.py
new file mode 100755
index 0000000..b14ad1a
--- /dev/null
+++ b/tools/api_sample_coverage.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, 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.
+
+'''
+Compare the R8 API used by the API usage sample to the API kept by @Keep.
+'''
+
+import argparse
+import os
+import subprocess
+import utils
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+                                 formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument('-o', '--output-dir')
+
+API_SAMPLE_JAR = 'tests/d8_api_usage_sample.jar'
+
+
+def main(output_dir=None):
+  if output_dir is None:
+    output_dir = ''
+
+  printseeds_path = os.path.join(output_dir, 'keep-seeds.txt')
+  printseeds_args = [
+    'java', '-jar', utils.R8_JAR, 'printseeds',
+    utils.RT_JAR, utils.R8_JAR, utils.R8LIB_KEEP_RULES,
+  ]
+  write_sorted_lines(printseeds_args, printseeds_path)
+
+  printuses_path = os.path.join(output_dir, 'sample-uses.txt')
+  printuses_args = [
+    'java', '-jar', utils.R8_JAR, 'printuses',
+    utils.RT_JAR, utils.R8_JAR, API_SAMPLE_JAR,
+  ]
+  write_sorted_lines(printuses_args, printuses_path)
+
+  print_diff(printseeds_path, printuses_path)
+
+
+def write_sorted_lines(cmd_args, output_path):
+  utils.PrintCmd(cmd_args)
+  output_lines = subprocess.check_output(cmd_args).splitlines(True)
+  print("Write output to %s" % output_path)
+  output_lines.sort()
+  with open(output_path, 'w') as fp:
+    for line in output_lines:
+      fp.write(line)
+
+
+def print_diff(printseeds_path, printuses_path):
+  with open(printseeds_path) as fp:
+    seeds = set(fp.read().splitlines())
+  with open(printuses_path) as fp:
+    uses = set(fp.read().splitlines())
+  only_in_seeds = seeds - uses
+  only_in_uses = uses - seeds
+  if only_in_seeds:
+    print("%s lines with '-' are marked @Keep " % len(only_in_seeds) +
+          "but not used by sample.")
+  if only_in_uses:
+    print("%s lines with '+' are used by sample " % len(only_in_uses) +
+          "but are missing @Keep annotations.")
+  for line in sorted(only_in_seeds):
+    print('-' + line)
+  for line in sorted(only_in_uses):
+    print('+' + line)
+  if not only_in_seeds and not only_in_uses:
+    print('Sample uses the entire set of members marked @Keep. Well done!')
+
+
+if __name__ == '__main__':
+  main(**vars(parser.parse_args()))
diff --git a/tools/build_r8lib.py b/tools/build_r8lib.py
index abaa619..4b7473e 100755
--- a/tools/build_r8lib.py
+++ b/tools/build_r8lib.py
@@ -18,7 +18,6 @@
                                  formatter_class=argparse.RawTextHelpFormatter)
 
 SAMPLE_JAR = os.path.join(utils.REPO_ROOT, 'tests/d8_api_usage_sample.jar')
-KEEP_RULES = os.path.join(utils.REPO_ROOT, 'src/main/keep.txt')
 R8LIB_JAR = os.path.join(utils.LIBS, 'r8lib.jar')
 R8LIB_MAP_FILE = os.path.join(utils.LIBS, 'r8lib-map.txt')
 
@@ -26,16 +25,21 @@
 ANDROID_JAR = 'third_party/android_jar/lib-v%s/android.jar' % API_LEVEL
 
 
-def build_r8lib():
+def build_r8lib(output_path=None, output_map=None, **kwargs):
+  if output_path is None:
+    output_path = R8LIB_JAR
+  if output_map is None:
+    output_map = R8LIB_MAP_FILE
   toolhelper.run(
       'r8',
       ('--release',
        '--classfile',
        '--lib', utils.RT_JAR,
        utils.R8_JAR,
-       '--output', R8LIB_JAR,
-       '--pg-conf', KEEP_RULES,
-       '--pg-map-output', R8LIB_MAP_FILE))
+       '--output', output_path,
+       '--pg-conf', utils.R8LIB_KEEP_RULES,
+       '--pg-map-output', output_map),
+      **kwargs)
 
 
 def test_d8sample():
diff --git a/tools/printseeds.py b/tools/printseeds.py
new file mode 100755
index 0000000..4f389d5
--- /dev/null
+++ b/tools/printseeds.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('printseeds', sys.argv[1:]))
diff --git a/tools/printuses.py b/tools/printuses.py
new file mode 100755
index 0000000..17d3df1
--- /dev/null
+++ b/tools/printuses.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('printuses', sys.argv[1:]))
diff --git a/tools/r8lib_size_compare.py b/tools/r8lib_size_compare.py
new file mode 100755
index 0000000..84dd1a2
--- /dev/null
+++ b/tools/r8lib_size_compare.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, 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.
+
+'''
+Build r8lib.jar with both R8 and ProGuard and print a size comparison.
+
+By default, inlining is disabled in both R8 and ProGuard to make
+method-by-method comparison much easier. Pass --inlining to enable inlining.
+
+By default, only shows methods where R8's DEX output is 5 or more instructions
+larger than ProGuard+D8's output. Pass --threshold 0 to display all methods.
+'''
+
+import argparse
+import build_r8lib
+import os
+import subprocess
+import toolhelper
+import utils
+
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+                                 formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument('-t', '--tmpdir',
+                    help='Store auxiliary files in given directory')
+parser.add_argument('-i', '--inlining', action='store_true',
+                    help='Enable inlining')
+parser.add_argument('--threshold')
+
+R8_RELOCATIONS = [
+  ('com.google.common', 'com.android.tools.r8.com.google.common'),
+  ('com.google.gson', 'com.android.tools.r8.com.google.gson'),
+  ('com.google.thirdparty', 'com.android.tools.r8.com.google.thirdparty'),
+  ('joptsimple', 'com.android.tools.r8.joptsimple'),
+  ('org.apache.commons', 'com.android.tools.r8.org.apache.commons'),
+  ('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm'),
+  ('it.unimi.dsi.fastutil', 'com.android.tools.r8.it.unimi.dsi.fastutil'),
+]
+
+
+def is_output_newer(input, output):
+  if not os.path.exists(output):
+    return False
+  return os.stat(input).st_mtime < os.stat(output).st_mtime
+
+
+def check_call(args, **kwargs):
+  utils.PrintCmd(args)
+  return subprocess.check_call(args, **kwargs)
+
+
+def main(tmpdir=None, inlining=True,
+         run_jarsizecompare=True, threshold=None):
+  if tmpdir is None:
+    with utils.TempDir() as tmpdir:
+      return main(tmpdir, inlining)
+
+  inline_suffix = '-inline' if inlining else '-noinline'
+
+  pg_config = utils.R8LIB_KEEP_RULES
+  r8lib_jar = os.path.join(utils.LIBS, 'r8lib%s.jar' % inline_suffix)
+  r8lib_map = os.path.join(utils.LIBS, 'r8lib%s-map.txt' % inline_suffix)
+  r8lib_args = None
+  if not inlining:
+    r8lib_args = ['-Dcom.android.tools.r8.disableinlining=1']
+    pg_config = os.path.join(tmpdir, 'keep-noinline.txt')
+    with open(pg_config, 'w') as new_config:
+      with open(utils.R8LIB_KEEP_RULES) as old_config:
+        new_config.write(old_config.read().rstrip('\n') +
+                         '\n-optimizations !method/inlining/*\n')
+
+  if not is_output_newer(utils.R8_JAR, r8lib_jar):
+    r8lib_memory = os.path.join(tmpdir, 'r8lib%s-memory.txt' % inline_suffix)
+    build_r8lib.build_r8lib(
+        output_path=r8lib_jar, output_map=r8lib_map,
+        extra_args=r8lib_args, track_memory_file=r8lib_memory)
+
+  pg_output = os.path.join(tmpdir, 'r8lib-pg%s.jar' % inline_suffix)
+  pg_memory = os.path.join(tmpdir, 'r8lib-pg%s-memory.txt' % inline_suffix)
+  pg_map = os.path.join(tmpdir, 'r8lib-pg%s-map.txt' % inline_suffix)
+  pg_args = ['tools/track_memory.sh', pg_memory,
+             'third_party/proguard/proguard6.0.2/bin/proguard.sh',
+             '@' + pg_config,
+             '-lib', utils.RT_JAR,
+             '-injar', utils.R8_JAR,
+             '-printmapping', pg_map,
+             '-outjar', pg_output]
+  for library_name, relocated_package in R8_RELOCATIONS:
+    pg_args.extend(['-dontwarn', relocated_package + '.**',
+                    '-dontnote', relocated_package + '.**'])
+  check_call(pg_args)
+  if threshold is None:
+    threshold = 5
+  toolhelper.run('jarsizecompare',
+                 ['--threshold', str(threshold),
+                  '--lib', utils.RT_JAR,
+                  '--input', 'input', utils.R8_JAR,
+                  '--input', 'r8', r8lib_jar, r8lib_map,
+                  '--input', 'pg', pg_output, pg_map])
+
+
+if __name__ == '__main__':
+  main(**vars(parser.parse_args()))
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index a7f509c..82a2824 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -9,7 +9,7 @@
 import utils
 
 def run(tool, args, build=None, debug=True,
-        profile=False, track_memory_file=None):
+        profile=False, track_memory_file=None, extra_args=None):
   if build is None:
     build, args = extract_build_from_args(args)
   if build:
@@ -18,6 +18,8 @@
   if track_memory_file:
     cmd.extend(['tools/track_memory.sh', track_memory_file])
   cmd.append('java')
+  if extra_args:
+    cmd.extend(extra_args)
   if debug:
     cmd.append('-ea')
   if profile:
diff --git a/tools/utils.py b/tools/utils.py
index 859da97..851f3bd 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -37,6 +37,7 @@
 MAVEN_ZIP = os.path.join(LIBS, 'r8.zip')
 GENERATED_LICENSE = os.path.join(GENERATED_LICENSE_DIR, 'LICENSE')
 RT_JAR = os.path.join(REPO_ROOT, 'third_party/openjdk/openjdk-rt-1.8/rt.jar')
+R8LIB_KEEP_RULES = os.path.join(REPO_ROOT, 'src/main/keep.txt')
 
 def PrintCmd(s):
   if type(s) is list: