Merge "Revert "From DexAnnotation to KotlinInfo""
diff --git a/LIBRARY-LICENSE b/LIBRARY-LICENSE
index 12674e3..bc69c40 100644
--- a/LIBRARY-LICENSE
+++ b/LIBRARY-LICENSE
@@ -48,3 +48,21 @@
   license: ASM license
   licenseUrl: http://asm.ow2.org/license.html
   url: http://asm.ow2.org/index.html
+- artifact: org.jetbrains.kotlin:kotlin-stdlib:+
+  name: org.jetbrains.kotlin:kotlin-stdlib
+  copyrightHolder: JetBrains s.r.o.
+  license: The Apache License, Version 2.0
+  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
+  url: https://kotlinlang.org/
+- artifact: org.jetbrains.kotlinx:kotlinx-metadata-jvm:+
+  name: org.jetbrains.kotlinx:kotlinx-metadata-jvm
+  copyrightHolder: JetBrains s.r.o.
+  license: The Apache License, Version 2.0
+  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
+  url: https://kotlinlang.org/
+- artifact: org.jetbrains:annotations:+
+  name: IntelliJ IDEA Annotations
+  copyrightHolder: JetBrains s.r.o.
+  license: The Apache Software License, Version 2.0
+  licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
+  url: http://www.jetbrains.org
diff --git a/build.gradle b/build.gradle
index 8ea2b3f..92d7471 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,7 +60,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..af83158 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -17,6 +17,7 @@
 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;
@@ -60,7 +61,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());
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
index baac9bc..425b3c3 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -16,6 +16,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;
@@ -60,7 +61,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(
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..3239dbe 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;
@@ -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();
         }
@@ -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..1783f30 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.2.23-dev";
 
   private Version() {
   }
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..871f6d8 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -34,6 +34,7 @@
 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;
@@ -572,7 +573,7 @@
       // For each input archive file, add all class files within.
       for (Path input : inputs) {
         if (isArchive(input)) {
-          try (ZipFile zipFile = new ZipFile(input.toFile())) {
+          try (ZipFile zipFile = new ZipFile(input.toFile(), 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/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/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..8771b72 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
@@ -9,7 +9,9 @@
 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 +22,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 +178,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 +333,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;
     }
@@ -678,6 +683,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..a52637f 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
@@ -482,7 +482,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;
               }));
@@ -705,7 +705,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 +776,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 +818,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..1926b14 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
@@ -96,7 +96,7 @@
           if (!invokedHolder.isClassType()) {
             continue;
           }
-          DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method);
+          DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method, invoke.getType());
           Invoke.Type invokeType = getInvokeType(invoke, actualTarget, invokedMethod);
           if (actualTarget != invokedMethod || invoke.getType() != invokeType) {
             Invoke newInvoke = Invoke.create(invokeType, actualTarget, null,
@@ -107,10 +107,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 +124,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 +134,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 +144,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 +153,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 +161,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 +169,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 +184,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 +192,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 +200,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 +216,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 +231,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);
       }
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/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 56e381b..e3ec746 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,75 @@
     }
   }
 
+  private void parseError(DexItem item, Origin origin, GenericSignatureFormatError e) {
+    StringBuilder message = new StringBuilder("Invalid class 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.annotations = rewriteGenericSignatures(clazz.annotations,
+          genericSignatureParser::parseClassSignature,
+          e -> parseError(clazz, clazz.getOrigin(), e));
       clazz.forEachField(field -> rewriteGenericSignatures(
-          field.annotations.annotations, genericSignatureParser::parseFieldSignature));
+          field.annotations, genericSignatureParser::parseFieldSignature,
+          e -> parseError(field, clazz.getOrigin(), e)));
       clazz.forEachMethod(method -> rewriteGenericSignatures(
-          method.annotations.annotations, genericSignatureParser::parseMethodSignature));
+          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);
   }
 
   /**
@@ -453,7 +509,6 @@
       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);
       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..5b549a3 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 generic 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 generic 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 generic 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/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/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
index a80e758..c9d34ee 100644
--- a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
@@ -181,7 +181,8 @@
     if (Log.ENABLED) {
       Log.debug(getClass(), "Merged %d classes.", numberOfMerges);
     }
-    return renamedMembersLense.build(application.dexItemFactory, graphLense);
+    return new VerticalClassMergerGraphLense(
+        renamedMembersLense.build(application.dexItemFactory, graphLense));
   }
 
   private class ClassMerger {
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..1bdeb7c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
@@ -0,0 +1,85 @@
+// 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.ImmutableSet;
+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;
+
+  public VerticalClassMergerGraphLense(GraphLense previousLense) {
+    this.previousLense = previousLense;
+  }
+
+  @Override
+  public DexType lookupType(DexType type) {
+    return previousLense.lookupType(type);
+  }
+
+  @Override
+  public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+    // TODO(christofferqa): If [type] is Type.SUPER and [method] has been merged into the class of
+    // [context], then return the DIRECT method that has been created for [method] by SimpleClass-
+    // Merger. Otherwise, return the VIRTUAL method corresponding to [method].
+    return previousLense.lookupMethod(method, context, type);
+  }
+
+  @Override
+  public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+    DexMethod result = lookupMethod(method);
+    if (result != null) {
+      return ImmutableSet.of(result);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Override
+  public DexField lookupField(DexField field) {
+    return previousLense.lookupField(field);
+  }
+
+  @Override
+  public boolean isContextFreeForMethods() {
+    return false;
+  }
+
+  @Override
+  public boolean isContextFreeForMethod(DexMethod method) {
+    // TODO(christofferqa): Should return false for methods where this graph lense is context
+    // sensitive.
+    return true;
+  }
+}
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..0f06ca8 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -22,6 +22,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;
@@ -49,22 +50,23 @@
   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);
-          if (archive.matchesFile(name)) {
-            if (isDexFile(name)) {
+          String name = entry.getName();
+          Path path = Paths.get(name);
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+          if (archive.matchesFile(path)) {
+            if (isDexFile(path)) {
               if (!ignoreDexInArchive) {
                 ProgramResource resource =
                     OneShotByteResource.create(
                         Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
                 dexResources.add(resource);
               }
-            } else if (isClassFile(name)) {
+            } else if (isClassFile(path)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
               ProgramResource resource =
                   OneShotByteResource.create(
@@ -105,7 +107,7 @@
 
   @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();
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/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..849377b 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -10,6 +10,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 +28,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();
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
index ad31005..2ef0123 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,7 +388,7 @@
     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()))) {
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
index 7230f55..2c2e8d7 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,7 +379,7 @@
     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()))) {
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/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..da6ef55 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,18 @@
 -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.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/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/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..087012c 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -41,6 +41,7 @@
         "filledarray.FilledArray",
         "hello.Hello",
         "ifstatements.IfStatements",
+        "inlining.Inlining",
         "instancevariable.InstanceVariable",
         "instanceofstring.InstanceofString",
         "invoke.Invoke",
@@ -109,6 +110,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 +152,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/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index d5d2c39..65181dc 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,46 @@
 // 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.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 +50,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 +87,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());
@@ -100,4 +113,146 @@
     assertTrue(inspector.clazz("classmerging.SubClassThatReferencesSuperMethod").isPresent());
   }
 
+  @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);
+  }
+
+  @Ignore("b/73958515")
+  @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);
+    // If access modifications are allowed then SimpleInterface should be merged into
+    // SimpleInterfaceImpl.
+    preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.SimpleInterfaceAccessTest",
+            "classmerging.pkg.SimpleInterfaceImplRetriever",
+            "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+    runTest(
+        main,
+        programFiles,
+        preservedClassNames,
+        getProguardConfig(EXAMPLE_KEEP, "-allowaccessmodification"));
+  }
+
+  @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, null));
+  }
+
+  private DexInspector runTest(
+      String main, Path[] programFiles, Set<String> preservedClassNames, String proguardConfig)
+      throws Exception {
+    AndroidApp input = readProgramFiles(programFiles);
+    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());
+    }
+    if (additionalRules != null) {
+      builder.append(additionalRules);
+    }
+    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/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/naming/GenericSignatureParserTest.java b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
new file mode 100644
index 0000000..1d1604b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
@@ -0,0 +1,472 @@
+// 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)
+      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 generic 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;>;"));
+    failingParseAction(
+        parser -> parser.parseFieldSignature("LOuter$InnerInterface<TU;>.Inner;"));
+    failingParseAction(
+        parser -> parser.parseMethodSignature("(LOuter$InnerInterface<TU;>.Inner;)V"));
+  }
+}
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..68cd36f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
@@ -0,0 +1,496 @@
+// 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;>LNotFound;";
+    testSingleClass("Outer", signature, this::noWarnings, inspector -> {
+      assertThat(inspector.clazz("NotFound"), not(isPresent()));
+      ClassSubject outer = inspector.clazz("Outer");
+      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 class 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 class 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 class 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 class 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 class 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/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/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/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..e7e882a 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 {
@@ -817,6 +898,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 +933,10 @@
     public abstract DexValue getStaticValue();
 
     public abstract boolean isRenamed();
+
+    public abstract String getOriginalSignatureAttribute();
+
+    public abstract String getFinalSignatureAttribute();
   }
 
   public class AbsentFieldSubject extends FieldSubject {
@@ -889,6 +985,16 @@
     public DexEncodedField getField() {
       return null;
     }
+
+    @Override
+    public String getOriginalSignatureAttribute() {
+      return null;
+    }
+
+    @Override
+    public String getFinalSignatureAttribute() {
+      return null;
+    }
   }
 
   public class FoundFieldSubject extends FieldSubject {
@@ -956,6 +1062,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 +1527,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: