Merge "Enable more errorprone checks."
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 92d7471..7f41caa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,6 +37,7 @@
     gsonVersion = '2.7'
     junitVersion = '4.12'
     kotlinVersion = '1.2.30'
+    kotlinExtMetadataJVMVersion = '0.0.2'
     protobufVersion = '3.0.0'
     smaliVersion = '2.2b4'
 }
@@ -74,6 +75,7 @@
 
 repositories {
     maven { url 'https://maven.google.com' }
+    maven { url 'https://kotlin.bintray.com/kotlinx' }
     mavenCentral()
 }
 
@@ -223,6 +225,7 @@
         exclude group: 'org.codehaus.mojo'
     })
     compile group: 'it.unimi.dsi', name: 'fastutil', version: fastutilVersion
+    compile "org.jetbrains.kotlinx:kotlinx-metadata-jvm:$kotlinExtMetadataJVMVersion"
     compile group: 'org.ow2.asm', name: 'asm', version: asmVersion
     compile group: 'org.ow2.asm', name: 'asm-commons', version: asmVersion
     compile group: 'org.ow2.asm', name: 'asm-tree', version: asmVersion
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/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 28e1f93..f54233b 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -61,6 +61,9 @@
       case "jardiff":
         JarDiff.main(shift(args));
         break;
+      case "jarsizecompare":
+        JarSizeCompare.main(shift(args));
+        break;
       case "maindex":
         GenerateMainDexList.main(shift(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/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/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..5f0fb83 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -264,9 +264,10 @@
 
   public void setCode(
       IRCode ir,
+      GraphLense graphLense,
       RegisterAllocator registerAllocator,
       InternalOptions options) {
-    final DexBuilder builder = new DexBuilder(ir, registerAllocator, options);
+    final DexBuilder builder = new DexBuilder(ir, graphLense, registerAllocator, options);
     code = builder.build(method.getArity());
   }
 
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/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index e68e3d5..ca79b0f 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
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 8201562..ee7df48 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -43,6 +43,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
 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;
@@ -81,6 +82,10 @@
   // The IR representation of the code to build.
   private final IRCode ir;
 
+  // Graph lense for building the exception handlers. Needed since program classes that inherit
+  // from Throwable may get merged into their subtypes during class merging.
+  private final GraphLense graphLense;
+
   // The register allocator providing register assignments for the code to build.
   private final RegisterAllocator registerAllocator;
 
@@ -116,11 +121,14 @@
 
   public DexBuilder(
       IRCode ir,
+      GraphLense graphLense,
       RegisterAllocator registerAllocator,
       InternalOptions options) {
     assert ir != null;
+    assert graphLense != null;
     assert registerAllocator != null;
     this.ir = ir;
+    this.graphLense = graphLense;
     this.registerAllocator = registerAllocator;
     this.options = options;
   }
@@ -706,7 +714,12 @@
           assert i == handlerGroup.getGuards().size() - 1;
           catchAllOffset = targetOffset;
         } else {
-          pairs.add(new TypeAddrPair(type, targetOffset));
+          // The type may have changed due to class merging.
+          // TODO(christofferqa): This assumes that the graph lense is context insensitive for the
+          // given type (which is always the case). Consider removing the context-argument from
+          // GraphLense.lookupType, since we do not currently have a use case for it.
+          DexType actualType = graphLense.lookupType(type, null);
+          pairs.add(new TypeAddrPair(actualType, targetOffset));
         }
       }
       TypeAddrPair[] pairsArray = pairs.toArray(new TypeAddrPair[pairs.size()]);
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..df4e69c 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
@@ -532,7 +532,7 @@
     assert code.isConsistentSSA();
     code.traceBlocks();
     RegisterAllocator registerAllocator = performRegisterAllocation(code, method);
-    method.setCode(code, registerAllocator, options);
+    method.setCode(code, graphLense, registerAllocator, options);
     if (Log.ENABLED) {
       Log.debug(getClass(), "Resulting dex code for %s:\n%s",
           method.toSourceString(), logCode(options, method));
@@ -784,7 +784,7 @@
   private void finalizeToDex(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     // Perform register allocation.
     RegisterAllocator registerAllocator = performRegisterAllocation(code, method);
-    method.setCode(code, registerAllocator, options);
+    method.setCode(code, graphLense, registerAllocator, options);
     updateHighestSortingStrings(method);
     if (Log.ENABLED) {
       Log.debug(getClass(), "Resulting dex code for %s:\n%s",
diff --git a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
index cf2b2bc..0af41b1 100644
--- a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
+++ b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
@@ -5,19 +5,11 @@
 package com.android.tools.r8.kotlin;
 
 import com.android.tools.r8.DiagnosticsHandler;
-import com.android.tools.r8.graph.DexAnnotation;
-import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexValue;
-import com.android.tools.r8.graph.DexValue.DexValueArray;
-import com.android.tools.r8.graph.DexValue.DexValueInt;
-import com.android.tools.r8.graph.DexValue.DexValueString;
-import com.android.tools.r8.kotlin.KotlinSyntheticClass.Flavour;
-import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
@@ -58,10 +50,13 @@
 
     public final DexString kotlinStyleLambdaInstanceName = factory.createString("INSTANCE");
 
+    public final DexType functionBase = factory.createType("Lkotlin/jvm/internal/FunctionBase;");
     public final DexType lambdaType = factory.createType("Lkotlin/jvm/internal/Lambda;");
 
-    public final DexMethod lambdaInitializerMethod = factory.createMethod(lambdaType,
-        factory.createProto(factory.voidType, factory.intType), factory.constructorMethodName);
+    public final DexMethod lambdaInitializerMethod = factory.createMethod(
+        lambdaType,
+        factory.createProto(factory.voidType, factory.intType),
+        factory.constructorMethodName);
 
     public boolean isFunctionInterface(DexType type) {
       return functions.contains(type);
@@ -70,9 +65,14 @@
 
   public final class Metadata {
     public final DexType kotlinMetadataType = factory.createType("Lkotlin/Metadata;");
-    public final DexString elementNameK = factory.createString("k");
-    public final DexString elementNameD1 = factory.createString("d1");
-    public final DexString elementNameD2 = factory.createString("d2");
+    public final DexString kind = factory.createString("k");
+    public final DexString metadataVersion = factory.createString("mv");
+    public final DexString bytecodeVersion = factory.createString("bv");
+    public final DexString data1 = factory.createString("d1");
+    public final DexString data2 = factory.createString("d2");
+    public final DexString extraString = factory.createString("xs");
+    public final DexString packageName = factory.createString("pn");
+    public final DexString extraInt = factory.createString("xi");
   }
 
   // kotlin.jvm.internal.Intrinsics class
@@ -86,98 +86,6 @@
 
   // Calculates kotlin info for a class.
   public KotlinInfo getKotlinInfo(DexClass clazz, DiagnosticsHandler reporter) {
-    if (clazz.annotations.isEmpty()) {
-      return null;
-    }
-    DexAnnotation meta = clazz.annotations.getFirstMatching(metadata.kotlinMetadataType);
-    if (meta != null) {
-      try {
-        return createKotlinInfo(clazz, meta);
-      } catch (MetadataError e) {
-        reporter.warning(
-            new StringDiagnostic("Class " + clazz.type.toSourceString() +
-                " has malformed kotlin.Metadata: " + e.getMessage()));
-      }
-    }
-    return null;
-  }
-
-  private KotlinInfo createKotlinInfo(DexClass clazz, DexAnnotation meta) {
-    DexAnnotationElement kindElement = getAnnotationElement(meta, metadata.elementNameK);
-    if (kindElement == null) {
-      throw new MetadataError("element 'k' is missing");
-    }
-
-    DexValue value = kindElement.value;
-    if (!(value instanceof DexValueInt)) {
-      throw new MetadataError("invalid 'k' value: " + value.toSourceString());
-    }
-
-    DexValueInt intValue = (DexValueInt) value;
-    switch (intValue.value) {
-      case 1:
-        return new KotlinClass();
-      case 2:
-        return new KotlinFile();
-      case 3:
-        return createSyntheticClass(clazz, meta);
-      case 4:
-        return new KotlinClassFacade();
-      case 5:
-        return new KotlinClassPart();
-      default:
-        throw new MetadataError("unsupported 'k' value: " + value.toSourceString());
-    }
-  }
-
-  private KotlinSyntheticClass createSyntheticClass(DexClass clazz, DexAnnotation meta) {
-    if (isKotlinStyleLambda(clazz)) {
-      return new KotlinSyntheticClass(Flavour.KotlinStyleLambda);
-    }
-    if (isJavaStyleLambda(clazz, meta)) {
-      return new KotlinSyntheticClass(Flavour.JavaStyleLambda);
-    }
-    return new KotlinSyntheticClass(Flavour.Unclassified);
-  }
-
-  private boolean isKotlinStyleLambda(DexClass clazz) {
-    // TODO: replace with direct hints from kotlin metadata when available.
-    return clazz.superType == this.functional.lambdaType;
-  }
-
-  private boolean isJavaStyleLambda(DexClass clazz, DexAnnotation meta) {
-    assert !isKotlinStyleLambda(clazz);
-    return clazz.superType == this.factory.objectType &&
-        clazz.interfaces.size() == 1 &&
-        isAnnotationElementNotEmpty(meta, metadata.elementNameD1);
-  }
-
-  private DexAnnotationElement getAnnotationElement(DexAnnotation annotation, DexString name) {
-    for (DexAnnotationElement element : annotation.annotation.elements) {
-      if (element.name == name) {
-        return element;
-      }
-    }
-    return null;
-  }
-
-  private boolean isAnnotationElementNotEmpty(DexAnnotation annotation, DexString name) {
-    for (DexAnnotationElement element : annotation.annotation.elements) {
-      if (element.name == name && element.value instanceof DexValueArray) {
-        DexValue[] values = ((DexValueArray) element.value).getValues();
-        if (values.length == 1 && values[0] instanceof DexValueString) {
-          return true;
-        }
-        // Must be broken metadata.
-        assert false;
-      }
-    }
-    return false;
-  }
-
-  private static class MetadataError extends RuntimeException {
-    MetadataError(String cause) {
-      super(cause);
-    }
+    return KotlinClassMetadataReader.getKotlinInfo(this, clazz, reporter);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java
index fc81994..74184a5 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClass.java
@@ -4,7 +4,31 @@
 
 package com.android.tools.r8.kotlin;
 
-public class KotlinClass extends KotlinInfo {
+import kotlinx.metadata.KmClassVisitor;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+public class KotlinClass extends KotlinInfo<KotlinClassMetadata.Class> {
+
+  static KotlinClass fromKotlinClassMetadata(KotlinClassMetadata kotlinClassMetadata) {
+    assert kotlinClassMetadata instanceof KotlinClassMetadata.Class;
+    KotlinClassMetadata.Class kClass = (KotlinClassMetadata.Class) kotlinClassMetadata;
+    return new KotlinClass(kClass);
+  }
+
+  private KotlinClass(KotlinClassMetadata.Class metadata) {
+    super(metadata);
+  }
+
+  @Override
+  void validateMetadata(KotlinClassMetadata.Class metadata) {
+    ClassMetadataVisitor visitor = new ClassMetadataVisitor();
+    // To avoid lazy parsing/verifying metadata.
+    metadata.accept(visitor);
+  }
+
+  private static class ClassMetadataVisitor extends KmClassVisitor {
+  }
+
   @Override
   public Kind getKind() {
     return Kind.Class;
@@ -20,6 +44,4 @@
     return this;
   }
 
-  KotlinClass() {
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java
index e829a27..62db3d1 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassFacade.java
@@ -4,7 +4,26 @@
 
 package com.android.tools.r8.kotlin;
 
-public final class KotlinClassFacade extends KotlinInfo {
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+public final class KotlinClassFacade extends KotlinInfo<KotlinClassMetadata.MultiFileClassFacade> {
+
+  static KotlinClassFacade fromKotlinClassMetadata(KotlinClassMetadata kotlinClassMetadata) {
+    assert kotlinClassMetadata instanceof KotlinClassMetadata.MultiFileClassFacade;
+    KotlinClassMetadata.MultiFileClassFacade multiFileClassFacade =
+        (KotlinClassMetadata.MultiFileClassFacade) kotlinClassMetadata;
+    return new KotlinClassFacade(multiFileClassFacade);
+  }
+
+  private KotlinClassFacade(KotlinClassMetadata.MultiFileClassFacade metadata) {
+    super(metadata);
+  }
+
+  @Override
+  void validateMetadata(KotlinClassMetadata.MultiFileClassFacade metadata) {
+    // No worries about lazy parsing/verifying, since no API to explore metadata details.
+  }
+
   @Override
   public Kind getKind() {
     return Kind.Facade;
@@ -20,7 +39,4 @@
     return this;
   }
 
-  KotlinClassFacade() {
-    super();
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
new file mode 100644
index 0000000..32aebff
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
@@ -0,0 +1,133 @@
+// 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.kotlin;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexString;
+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.utils.StringDiagnostic;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import kotlinx.metadata.InconsistentKotlinMetadataException;
+import kotlinx.metadata.jvm.KotlinClassHeader;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+final class KotlinClassMetadataReader {
+
+  static KotlinInfo getKotlinInfo(
+      Kotlin kotlin,
+      DexClass clazz,
+      DiagnosticsHandler reporter) {
+    if (clazz.annotations.isEmpty()) {
+      return null;
+    }
+    DexAnnotation meta = clazz.annotations.getFirstMatching(kotlin.metadata.kotlinMetadataType);
+    if (meta != null) {
+      try {
+        return createKotlinInfo(kotlin, clazz, meta);
+      } catch (ClassCastException | InconsistentKotlinMetadataException | MetadataError e) {
+        reporter.warning(
+            new StringDiagnostic("Class " + clazz.type.toSourceString()
+                + " has malformed kotlin.Metadata: " + e.getMessage()));
+      } catch (Throwable e) {
+         reporter.warning(
+            new StringDiagnostic("Unexpected error while reading " + clazz.type.toSourceString()
+                + "'s kotlin.Metadata: " + e.getMessage()));
+      }
+    }
+    return null;
+  }
+
+  private static KotlinInfo createKotlinInfo(
+      Kotlin kotlin,
+      DexClass clazz,
+      DexAnnotation meta) {
+    Map<DexString, DexAnnotationElement> elementMap = new IdentityHashMap<>();
+    for (DexAnnotationElement element : meta.annotation.elements) {
+      elementMap.put(element.name, element);
+    }
+
+    DexAnnotationElement kind = elementMap.get(kotlin.metadata.kind);
+    if (kind == null) {
+      throw new MetadataError("element 'k' is missing.");
+    }
+    Integer k = (Integer) kind.value.getBoxedValue();
+    DexAnnotationElement metadataVersion = elementMap.get(kotlin.metadata.metadataVersion);
+    int[] mv = metadataVersion == null ? null : getUnboxedIntArray(metadataVersion.value, "mv");
+    DexAnnotationElement bytecodeVersion = elementMap.get(kotlin.metadata.bytecodeVersion);
+    int[] bv = bytecodeVersion == null ? null : getUnboxedIntArray(bytecodeVersion.value, "bv");
+    DexAnnotationElement data1 = elementMap.get(kotlin.metadata.data1);
+    String[] d1 = data1 == null ? null : getUnboxedStringArray(data1.value, "d1");
+    DexAnnotationElement data2 = elementMap.get(kotlin.metadata.data2);
+    String[] d2 = data2 == null ? null : getUnboxedStringArray(data2.value, "d2");
+    DexAnnotationElement extraString = elementMap.get(kotlin.metadata.extraString);
+    String xs = extraString == null ? null : getUnboxedString(extraString.value, "xs");
+    DexAnnotationElement packageName = elementMap.get(kotlin.metadata.packageName);
+    String pn = packageName == null ? null : getUnboxedString(packageName.value, "pn");
+    DexAnnotationElement extraInt = elementMap.get(kotlin.metadata.extraInt);
+    Integer xi = extraInt == null ? null : (Integer) extraInt.value.getBoxedValue();
+
+    KotlinClassHeader header = new KotlinClassHeader(k, mv, bv, d1, d2, xs, pn, xi);
+    KotlinClassMetadata kMetadata = KotlinClassMetadata.read(header);
+
+    if (kMetadata instanceof KotlinClassMetadata.Class) {
+      return KotlinClass.fromKotlinClassMetadata(kMetadata);
+    } else if (kMetadata instanceof KotlinClassMetadata.FileFacade) {
+      return KotlinFile.fromKotlinClassMetadata(kMetadata);
+    } else if (kMetadata instanceof KotlinClassMetadata.MultiFileClassFacade) {
+      return KotlinClassFacade.fromKotlinClassMetadata(kMetadata);
+    } else if (kMetadata instanceof KotlinClassMetadata.MultiFileClassPart) {
+      return KotlinClassPart.fromKotlinClassMetdata(kMetadata);
+    } else if (kMetadata instanceof KotlinClassMetadata.SyntheticClass) {
+      return KotlinSyntheticClass.fromKotlinClassMetadata(kMetadata, kotlin, clazz);
+    } else {
+      throw new MetadataError("unsupported 'k' value: " + k);
+    }
+  }
+
+  private static int[] getUnboxedIntArray(DexValue v, String elementName) {
+    if (!(v instanceof DexValueArray)) {
+      throw new MetadataError("invalid '" + elementName + "' value: " + v.toSourceString());
+    }
+    DexValueArray intArrayValue = (DexValueArray) v;
+    DexValue[] values = intArrayValue.getValues();
+    int[] result = new int [values.length];
+    for (int i = 0; i < values.length; i++) {
+      result[i] = (Integer) values[i].getBoxedValue();
+    }
+    return result;
+  }
+
+  private static String[] getUnboxedStringArray(DexValue v, String elementName) {
+    if (!(v instanceof DexValueArray)) {
+      throw new MetadataError("invalid '" + elementName + "' value: " + v.toSourceString());
+    }
+    DexValueArray stringArrayValue = (DexValueArray) v;
+    DexValue[] values = stringArrayValue.getValues();
+    String[] result = new String [values.length];
+    for (int i = 0; i < values.length; i++) {
+      result[i] = getUnboxedString(values[i], elementName + "[" + i + "]");
+    }
+    return result;
+  }
+
+  private static String getUnboxedString(DexValue v, String elementName) {
+    if (!(v instanceof DexValueString)) {
+      throw new MetadataError("invalid '" + elementName + "' value: " + v.toSourceString());
+    }
+    return ((DexValueString) v).getValue().toString();
+  }
+
+  private static class MetadataError extends RuntimeException {
+    MetadataError(String cause) {
+      super(cause);
+    }
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java
index d6da817..da66e6c 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassPart.java
@@ -4,7 +4,31 @@
 
 package com.android.tools.r8.kotlin;
 
-public final class KotlinClassPart extends KotlinInfo {
+import kotlinx.metadata.KmPackageVisitor;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+public final class KotlinClassPart extends KotlinInfo<KotlinClassMetadata.MultiFileClassPart> {
+
+  static KotlinClassPart fromKotlinClassMetdata(KotlinClassMetadata kotlinClassMetadata) {
+    assert kotlinClassMetadata instanceof KotlinClassMetadata.MultiFileClassPart;
+    KotlinClassMetadata.MultiFileClassPart multiFileClassPart =
+        (KotlinClassMetadata.MultiFileClassPart) kotlinClassMetadata;
+    return new KotlinClassPart(multiFileClassPart);
+  }
+
+  private KotlinClassPart(KotlinClassMetadata.MultiFileClassPart metadata) {
+    super(metadata);
+  }
+
+  @Override
+  void validateMetadata(KotlinClassMetadata.MultiFileClassPart metadata) {
+    // To avoid lazy parsing/verifying metadata.
+    metadata.accept(new MultiFileClassPartMetadataVisitor());
+  }
+
+  private static class MultiFileClassPartMetadataVisitor extends KmPackageVisitor {
+  }
+
   @Override
   public Kind getKind() {
     return Kind.Part;
@@ -20,6 +44,4 @@
     return this;
   }
 
-  KotlinClassPart() {
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java b/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java
index 38a77ad..bcb70ed 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinFile.java
@@ -4,7 +4,31 @@
 
 package com.android.tools.r8.kotlin;
 
-public final class KotlinFile extends KotlinInfo {
+import kotlinx.metadata.KmPackageVisitor;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+public final class KotlinFile extends KotlinInfo<KotlinClassMetadata.FileFacade> {
+
+  static KotlinFile fromKotlinClassMetadata(KotlinClassMetadata kotlinClassMetadata) {
+    assert kotlinClassMetadata instanceof KotlinClassMetadata.FileFacade;
+    KotlinClassMetadata.FileFacade fileFacade =
+        (KotlinClassMetadata.FileFacade) kotlinClassMetadata;
+    return new KotlinFile(fileFacade);
+  }
+
+  private KotlinFile(KotlinClassMetadata.FileFacade metadata) {
+    super(metadata);
+  }
+
+  @Override
+  void validateMetadata(KotlinClassMetadata.FileFacade metadata) {
+    // To avoid lazy parsing/verifying metadata.
+    metadata.accept(new FileFacadeMetadataVisitor());
+  }
+
+  private static class FileFacadeMetadataVisitor extends KmPackageVisitor {
+  }
+
   @Override
   public Kind getKind() {
     return Kind.File;
@@ -20,6 +44,4 @@
     return this;
   }
 
-  KotlinFile() {
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java
index 4043bc6..702e0eb 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinInfo.java
@@ -4,11 +4,23 @@
 
 package com.android.tools.r8.kotlin;
 
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
 // Provides access to kotlin information.
-public abstract class KotlinInfo {
+public abstract class KotlinInfo<MetadataKind extends KotlinClassMetadata> {
+  MetadataKind metadata;
+
   KotlinInfo() {
   }
 
+  KotlinInfo(MetadataKind metadata) {
+    validateMetadata(metadata);
+    this.metadata = metadata;
+  }
+
+  // Subtypes will define how to validate the given metadata.
+  abstract void validateMetadata(MetadataKind metadata);
+
   public enum Kind {
     Class, File, Synthetic, Part, Facade
   }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
index 68721bb..4660800 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinSyntheticClass.java
@@ -4,7 +4,11 @@
 
 package com.android.tools.r8.kotlin;
 
-public final class KotlinSyntheticClass extends KotlinInfo {
+import com.android.tools.r8.graph.DexClass;
+import kotlinx.metadata.KmLambdaVisitor;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+
+public final class KotlinSyntheticClass extends KotlinInfo<KotlinClassMetadata.SyntheticClass> {
   public enum Flavour {
     KotlinStyleLambda,
     JavaStyleLambda,
@@ -13,12 +17,40 @@
 
   private final Flavour flavour;
 
-  KotlinSyntheticClass(Flavour flavour) {
+  static KotlinSyntheticClass fromKotlinClassMetadata(
+      KotlinClassMetadata kotlinClassMetadata, Kotlin kotlin, DexClass clazz) {
+    assert kotlinClassMetadata instanceof KotlinClassMetadata.SyntheticClass;
+    KotlinClassMetadata.SyntheticClass syntheticClass =
+        (KotlinClassMetadata.SyntheticClass) kotlinClassMetadata;
+    if (isKotlinStyleLambda(syntheticClass, kotlin, clazz)) {
+      return new KotlinSyntheticClass(Flavour.KotlinStyleLambda, syntheticClass);
+    } else if (isJavaStyleLambda(syntheticClass, kotlin, clazz)) {
+      return new KotlinSyntheticClass(Flavour.JavaStyleLambda, syntheticClass);
+    } else {
+      return new KotlinSyntheticClass(Flavour.Unclassified, syntheticClass);
+    }
+  }
+
+  private KotlinSyntheticClass(Flavour flavour, KotlinClassMetadata.SyntheticClass metadata) {
     this.flavour = flavour;
+    validateMetadata(metadata);
+    this.metadata = metadata;
+  }
+
+  @Override
+  void validateMetadata(KotlinClassMetadata.SyntheticClass metadata) {
+    if (metadata.isLambda()) {
+      SyntheticClassMetadataVisitor visitor = new SyntheticClassMetadataVisitor();
+      // To avoid lazy parsing/verifying metadata.
+      metadata.accept(visitor);
+    }
+  }
+
+  private static class SyntheticClassMetadataVisitor extends KmLambdaVisitor {
   }
 
   public boolean isLambda() {
-    return flavour == Flavour.KotlinStyleLambda || flavour == Flavour.JavaStyleLambda;
+    return isKotlinStyleLambda() || isJavaStyleLambda();
   }
 
   public boolean isKotlinStyleLambda() {
@@ -43,4 +75,31 @@
   public KotlinSyntheticClass asSyntheticClass() {
     return this;
   }
+
+  /**
+   * Returns {@code true} if the given {@link DexClass} is a Kotlin-style lambda:
+   *   a class that
+   *     1) is recognized as lambda in its Kotlin metadata;
+   *     2) directly extends kotlin.jvm.internal.Lambda
+   */
+  private static boolean isKotlinStyleLambda(
+      KotlinClassMetadata.SyntheticClass metadata, Kotlin kotlin, DexClass clazz) {
+    return metadata.isLambda()
+        && clazz.superType == kotlin.functional.lambdaType;
+  }
+
+  /**
+   * Returns {@code true} if the given {@link DexClass} is a Java-style lambda:
+   *   a class that
+   *     1) is recognized as lambda in its Kotlin metadata;
+   *     2) doesn't extend any other class;
+   *     3) directly implements only one Java SAM.
+   */
+  private static boolean isJavaStyleLambda(
+      KotlinClassMetadata.SyntheticClass metadata, Kotlin kotlin, DexClass clazz) {
+    return metadata.isLambda()
+        && clazz.superType == kotlin.factory.objectType
+        && clazz.interfaces.size() == 1;
+  }
+
 }
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/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/ExceptionTest.java b/src/test/examples/classmerging/ExceptionTest.java
new file mode 100644
index 0000000..b365b2a
--- /dev/null
+++ b/src/test/examples/classmerging/ExceptionTest.java
@@ -0,0 +1,29 @@
+// 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 {
+      throw new ExceptionB("Ouch!");
+    } catch (ExceptionA exception) {
+      System.out.println("Caught exception: " + exception.getMessage());
+    }
+  }
+
+  // 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);
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 1cc5b87..f9e6d52 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,9 @@
 -keep public class classmerging.Test {
   public static void main(...);
 }
+-keep public class classmerging.ExceptionTest {
+  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/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 8038c98..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(
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..348dbf1 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -109,6 +109,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 +151,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/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..69f925c 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,34 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 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.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
 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.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.Test;
-import org.junit.rules.TemporaryFolder;
 
-public class ClassMergingTest {
+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 +38,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 +75,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 +101,39 @@
     assertTrue(inspector.clazz("classmerging.SubClassThatReferencesSuperMethod").isPresent());
   }
 
+  // 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")
+        };
+    Set<String> preservedClassNames =
+        ImmutableSet.of("classmerging.ExceptionTest", "classmerging.ExceptionTest$ExceptionB");
+    runTest(main, programFiles, preservedClassNames);
+  }
+
+  private void runTest(String main, Path[] programFiles, Set<String> preservedClassNames)
+      throws Exception {
+    AndroidApp input = readProgramFiles(programFiles);
+    AndroidApp output = compileWithR8(input, EXAMPLE_KEEP, 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));
+  }
 }
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/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 4214a67..d6e3612 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -38,6 +38,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.code.CatchHandlers;
@@ -652,7 +653,7 @@
                 code);
         IRCode ir = code.buildIR(method, null, options, Origin.unknown());
         RegisterAllocator allocator = new LinearScanRegisterAllocator(ir, options);
-        method.setCode(ir, allocator, options);
+        method.setCode(ir, GraphLense.getIdentityLense(), allocator, options);
         directMethods[i] = method;
       }
       builder.addProgramClass(
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..751ac83
--- /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 onr 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("" + i + " Was: " + e.getMessage(), 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/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/Regress78493232_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
index 3a56004..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
@@ -30,7 +30,7 @@
               Regress78493232Dump_WithPhi.dump(),
               ToolHelper.getClassAsBytes(Regress78493232Utils.class));
       ProcessResult javaResult =
-          runOnJava(
+          runOnJavaRaw(
               Regress78493232Dump_WithPhi.CLASS_NAME,
               Regress78493232Dump_WithPhi.dump(),
               ToolHelper.getClassAsBytes(Regress78493232Utils.class));
diff --git a/tools/build_r8lib.py b/tools/build_r8lib.py
index c855bc5..4b7473e 100755
--- a/tools/build_r8lib.py
+++ b/tools/build_r8lib.py
@@ -25,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,
+       '--output', output_path,
        '--pg-conf', utils.R8LIB_KEEP_RULES,
-       '--pg-map-output', R8LIB_MAP_FILE))
+       '--pg-map-output', output_map),
+      **kwargs)
 
 
 def test_d8sample():
diff --git a/tools/r8lib_size_compare.py b/tools/r8lib_size_compare.py
new file mode 100755
index 0000000..4bb49eb
--- /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 utils.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: