Merge "Let MinifiedNameMapPrinter use StringBuilder"
diff --git a/src/main/java/com/android/tools/r8/ClassFileConsumer.java b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
new file mode 100644
index 0000000..f55e362
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
@@ -0,0 +1,184 @@
+// 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 static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.IOExceptionDiagnostic;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Consumer for Java classfile encoded programs.
+ *
+ * <p>This consumer can only be provided to R8.
+ */
+public interface ClassFileConsumer extends ProgramConsumer {
+
+  /**
+   * Callback to receive Java classfile data for a compilation output.
+   *
+   * <p>There is no guaranteed order and files might be written concurrently.
+   *
+   * <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 Java class-file encoded data.
+   * @param descriptor Class descriptor of the class the data pertains to.
+   * @param handler Diagnostics handler for reporting.
+   */
+  void accept(byte[] data, String descriptor, DiagnosticsHandler handler);
+
+  /** Empty consumer to request the production of the resource but ignore its value. */
+  class EmptyConsumer implements ClassFileConsumer {
+
+    @Override
+    public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
+      // Ignore data.
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      // Nothing to close.
+    }
+  }
+
+  /** Forwarding consumer to delegate to an optional existing consumer. */
+  class ForwardingConsumer implements ClassFileConsumer {
+
+    private final ClassFileConsumer consumer;
+
+    public ForwardingConsumer(ClassFileConsumer consumer) {
+      this.consumer = consumer;
+    }
+
+    @Override
+    public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.accept(data, descriptor, handler);
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (consumer != null) {
+        consumer.finished(handler);
+      }
+    }
+  }
+
+  /** Archive consumer to write program resources to a zip archive. */
+  class ArchiveConsumer extends ForwardingConsumer {
+
+    private final Path archive;
+    private final Origin origin;
+    private ZipOutputStream stream = null;
+    private boolean closed = false;
+
+    public ArchiveConsumer(Path archive) {
+      this(archive, null);
+    }
+
+    public ArchiveConsumer(Path archive, ClassFileConsumer consumer) {
+      super(consumer);
+      this.archive = archive;
+      origin = new PathOrigin(archive);
+    }
+
+    @Override
+    public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
+      super.accept(data, descriptor, handler);
+      synchronizedWrite(getClassFileName(descriptor), data, handler);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+      assert !closed;
+      closed = true;
+      try {
+        if (stream != null) {
+          stream.close();
+          stream = null;
+        }
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    private ZipOutputStream getStream(DiagnosticsHandler handler) {
+      assert !closed;
+      if (stream == null) {
+        try {
+          stream =
+              new ZipOutputStream(
+                  Files.newOutputStream(
+                      archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+        } catch (IOException e) {
+          handler.error(new IOExceptionDiagnostic(e, origin));
+        }
+      }
+      return stream;
+    }
+
+    private synchronized void synchronizedWrite(
+        String entry, byte[] content, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(getStream(handler), entry, content);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, origin));
+      }
+    }
+
+    private static String getClassFileName(String classDescriptor) {
+      assert classDescriptor != null && DescriptorUtils.isClassDescriptor(classDescriptor);
+      return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + CLASS_EXTENSION;
+    }
+  }
+
+  /** Directory consumer to write program resources to a directory. */
+  class DirectoryConsumer extends ForwardingConsumer {
+
+    private final Path directory;
+
+    public DirectoryConsumer(Path directory) {
+      this(directory, null);
+    }
+
+    public DirectoryConsumer(Path directory, ClassFileConsumer consumer) {
+      super(consumer);
+      this.directory = directory;
+    }
+
+    @Override
+    public void accept(byte[] data, String descriptor, DiagnosticsHandler handler) {
+      super.accept(data, descriptor, handler);
+      Path target = directory.resolve(ArchiveConsumer.getClassFileName(descriptor));
+      try {
+        writeFileFromDescriptor(data, target);
+      } catch (IOException e) {
+        handler.error(new IOExceptionDiagnostic(e, new PathOrigin(target)));
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      super.finished(handler);
+    }
+
+    private static void writeFileFromDescriptor(byte[] contents, Path target) throws IOException {
+      Files.createDirectories(target.getParent());
+      FileUtils.writeToFile(target, null, contents);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index adb780e..0e7f329 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -217,6 +217,7 @@
       R8.unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
+      options.closeProgramConsumer();
       outputSink.close();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/OutputSink.java b/src/main/java/com/android/tools/r8/OutputSink.java
index 316df01..a8d934a 100644
--- a/src/main/java/com/android/tools/r8/OutputSink.java
+++ b/src/main/java/com/android/tools/r8/OutputSink.java
@@ -54,28 +54,6 @@
       throws IOException;
 
   /**
-   * Write a Java classfile that contains the class primaryClassName and its companion classes.
-   * <p>
-   * This is equivalent to writing out the file com/foo/bar/Test.class given a primaryClassName of
-   * com.foo.bar.Test.
-   * <p>
-   * There is no guaranteed order and files might be written concurrently. However, for each
-   * primaryClassName only one file is ever written.
-   * <p>
-   * This method is only invoked by R8 and only if compiling to Java bytecode. If this method is
-   * called, the other writeDexFile and writeClassFile methods will not be called.
-   */
-  void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException;
-
-  /**
-   * Provides the raw bytes that would be generated for the <code>-printseeds</code> flag.
-   * <p>
-   * This method is only invoked by R8 and only if R8 is instructed to generate seeds information.
-   */
-  void writeProguardSeedsFile(byte[] contents) throws IOException;
-
-  /**
    * Closes the output sink.
    * <p>
    * This method is invokes once all output has been generated.
diff --git a/src/main/java/com/android/tools/r8/ProgramConsumer.java b/src/main/java/com/android/tools/r8/ProgramConsumer.java
new file mode 100644
index 0000000..061741c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ProgramConsumer.java
@@ -0,0 +1,19 @@
+// 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;
+
+/**
+ * Base for all program consumers to allow abstracting which concrete consumer is provided to D8/R8.
+ */
+public interface ProgramConsumer {
+
+  /**
+   * Callback signifying that compilation of program resources has finished.
+   *
+   * <p>Called only once after all program outputs have been generated and consumed.
+   *
+   * @param handler Diagnostics handler for reporting.
+   */
+  void finished(DiagnosticsHandler handler);
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 73a5b1a..3e30ebf 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -106,14 +106,15 @@
       OutputSink outputSink,
       String deadCode,
       NamingLens namingLens,
-      byte[] proguardSeedsData,
+      String proguardSeedsData,
       InternalOptions options,
       ProguardMapSupplier proguardMapSupplier)
       throws ExecutionException, DexOverflowException {
     try {
       Marker marker = getMarker(options);
-      if (options.outputClassFiles) {
-        new CfApplicationWriter(application, options).write(outputSink, executorService);
+      if (options.isGeneratingClassFiles()) {
+        new CfApplicationWriter(application, options)
+            .write(options.getClassFileConsumer(), executorService);
       } else {
         new ApplicationWriter(
                 application,
@@ -173,7 +174,7 @@
 
       AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
       RootSet rootSet;
-      byte[] proguardSeedsData = null;
+      String proguardSeedsData = null;
       timing.begin("Strip unused code");
       try {
         Set<DexType> missingClasses = appInfo.getMissingClasses();
@@ -202,7 +203,7 @@
           PrintStream out = new PrintStream(bytes);
           RootSetBuilder.writeSeeds(appInfo.withLiveness(), out);
           out.flush();
-          proguardSeedsData = bytes.toByteArray();
+          proguardSeedsData = bytes.toString();
         }
         if (options.useTreeShaking) {
           TreePruner pruner = new TreePruner(application, appInfo.withLiveness(), options);
@@ -392,6 +393,7 @@
       throw new AssertionError(e); // unwrapping method should have thrown
     } finally {
       outputSink.close();
+      options.closeProgramConsumer();
       // Dump timings.
       if (options.printTimes) {
         timing.report();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index d2455f3..dd8d0ff 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -569,6 +569,13 @@
           : new StringConsumer.StreamConsumer(StandardOutOrigin.instance(), System.out);
     }
 
+    // Setup pg-seeds consumer.
+    if (proguardConfiguration.isPrintSeeds()) {
+      internal.proguardSeedsConsumer =  proguardConfiguration.getSeedFile() != null
+          ? new StringConsumer.FileConsumer(proguardConfiguration.getSeedFile())
+          : new StringConsumer.StreamConsumer(StandardOutOrigin.instance(), System.out);
+    }
+
     // Amend the proguard-map consumer with options from the proguard configuration.
     {
       StringConsumer wrappedConsumer;
diff --git a/src/main/java/com/android/tools/r8/StringConsumer.java b/src/main/java/com/android/tools/r8/StringConsumer.java
index 9e5ad47..33f364e 100644
--- a/src/main/java/com/android/tools/r8/StringConsumer.java
+++ b/src/main/java/com/android/tools/r8/StringConsumer.java
@@ -25,6 +25,7 @@
    * <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 string String resource.
    * @param handler Diagnostics handler for reporting.
    */
@@ -141,8 +142,11 @@
     @Override
     public void accept(String string, DiagnosticsHandler handler) {
       super.accept(string, handler);
-      try (BufferedWriter writer =
-          new BufferedWriter(new OutputStreamWriter(outputStream, encoding.newEncoder()))) {
+      // Don't close this writer as it will close the underlying stream, which we specifically do
+      // not want.
+      BufferedWriter writer =
+          new BufferedWriter(new OutputStreamWriter(outputStream, encoding.newEncoder()));
+      try {
         writer.write(string);
       } catch (IOException e) {
         handler.error(new IOExceptionDiagnostic(e, origin));
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 76c756b..0d92201 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -45,7 +45,7 @@
   public final DexApplication application;
   public final String deadCode;
   public final NamingLens namingLens;
-  public final byte[] proguardSeedsData;
+  public final String proguardSeedsData;
   public final InternalOptions options;
   public DexString markerString;
   public final ProguardMapSupplier proguardMapSupplier;
@@ -113,7 +113,7 @@
       Marker marker,
       String deadCode,
       NamingLens namingLens,
-      byte[] proguardSeedsData,
+      String proguardSeedsData,
       ProguardMapSupplier proguardMapSupplier) {
     assert application != null;
     this.application = application;
@@ -208,22 +208,21 @@
 
       if (options.usageInformationConsumer != null && deadCode != null) {
         ExceptionUtils.withConsumeResourceHandler(
-            options.reporter, deadCode, options.usageInformationConsumer);
+            options.reporter, options.usageInformationConsumer, deadCode);
       }
       // Write the proguard map file after writing the dex files, as the map writer traverses
       // the DexProgramClass structures, which are destructively updated during dex file writing.
       if (proguardMapSupplier != null && options.proguardMapConsumer != null) {
         ExceptionUtils.withConsumeResourceHandler(
-            options.reporter, proguardMapSupplier.get(), options.proguardMapConsumer);
+            options.reporter, options.proguardMapConsumer, proguardMapSupplier.get());
       }
-      if (options.proguardConfiguration.isPrintSeeds()) {
-        if (proguardSeedsData != null) {
-          outputSink.writeProguardSeedsFile(proguardSeedsData);
-        }
+      if (options.proguardSeedsConsumer != null && proguardSeedsData != null) {
+        ExceptionUtils.withConsumeResourceHandler(
+            options.reporter, options.proguardSeedsConsumer, proguardSeedsData);
       }
       if (options.mainDexListConsumer != null) {
         ExceptionUtils.withConsumeResourceHandler(
-            options.reporter, writeMainDexList(), options.mainDexListConsumer);
+            options.reporter, options.mainDexListConsumer, writeMainDexList());
       }
     } finally {
       application.timing.end();
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index 88c187b..da6fca3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -105,8 +105,8 @@
       }
     }
 
-    if (!isBlockExit && emittedPc != pc && pcAdvancing) {
-      // For non-exit / pc-advancing instructions emit any pending changes.
+    if (emittedPc != pc && pcAdvancing) {
+      // For pc-advancing instructions emit any pending changes.
       emitLocalChanges(pc);
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 4bc9a50..8557054 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.IdentifierUtils;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.UTFDataFormatException;
 import java.util.Arrays;
@@ -206,37 +207,6 @@
     return slowCompareTo(other);
   }
 
-  private boolean isSimpleNameChar(char ch) {
-    if (ch >= 'A' && ch <= 'Z') {
-      return true;
-    }
-    if (ch >= 'a' && ch <= 'z') {
-      return true;
-    }
-    if (ch >= '0' && ch <= '9') {
-      return true;
-    }
-    if (ch == '$' || ch == '-' || ch == '_') {
-      return true;
-    }
-    if (ch >= 0x00a1 && ch <= 0x1fff) {
-      return true;
-    }
-    if (ch >= 0x2010 && ch <= 0x2027) {
-      return true;
-    }
-    if (ch >= 0x2030 && ch <= 0xd7ff) {
-      return true;
-    }
-    if (ch >= 0xe000 && ch <= 0xffef) {
-      return true;
-    }
-    if (ch >= 0x10000 && ch <= 0x10ffff) {
-      return true;
-    }
-    return false;
-  }
-
   private boolean isValidClassDescriptor(String string) {
     if (string.length() < 3
         || string.charAt(0) != 'L'
@@ -251,7 +221,7 @@
       if (ch == '/') {
         continue;
       }
-      if (isSimpleNameChar(ch)) {
+      if (IdentifierUtils.isDexIdentifierPart(ch)) {
         continue;
       }
       return false;
@@ -273,7 +243,7 @@
     }
     for (int i = 0; i < string.length(); i++) {
       char ch = string.charAt(i);
-      if (isSimpleNameChar(ch)) {
+      if (IdentifierUtils.isDexIdentifierPart(ch)) {
         continue;
       }
       return false;
@@ -296,7 +266,7 @@
       }
     }
     for (int i = start; i < end; i++) {
-      if (isSimpleNameChar(string.charAt(i))) {
+      if (IdentifierUtils.isDexIdentifierPart(string.charAt(i))) {
         continue;
       }
       return false;
diff --git a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
index 6bf1a17..14c7a6b 100644
--- a/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarApplicationReader.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.errors.InternalCompilerError;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
@@ -21,12 +22,24 @@
 
   public final InternalOptions options;
 
+  private ConcurrentHashMap<String, Type> asmObjectTypeCache = new ConcurrentHashMap<>();
+
+  private ConcurrentHashMap<String, Type> asmTypeCache = new ConcurrentHashMap<>();
+
   ConcurrentHashMap<String, DexString> stringCache = new ConcurrentHashMap<>();
 
   public JarApplicationReader(InternalOptions options) {
     this.options = options;
   }
 
+  public Type getAsmObjectType(String name) {
+    return asmObjectTypeCache.computeIfAbsent(name, (key) -> Type.getObjectType(key));
+  }
+
+  public Type getAsmType(String name) {
+    return asmTypeCache.computeIfAbsent(name, (key) -> Type.getType(key));
+  }
+
   public DexItemFactory getFactory() {
     return options.itemFactory;
   }
@@ -41,7 +54,7 @@
 
   public DexType getTypeFromName(String name) {
     assert isValidInternalName(name);
-    return getType(Type.getObjectType(name));
+    return getType(getAsmObjectType(name));
   }
 
   public DexType getTypeFromDescriptor(String desc) {
@@ -101,9 +114,8 @@
 
   public DexProto getProto(String desc) {
     assert isValidDescriptor(desc);
-    Type returnType = Type.getReturnType(desc);
-    Type[] arguments = Type.getArgumentTypes(desc);
-
+    Type returnType = getReturnType(desc);
+    Type[] arguments = getArgumentTypes(desc);
     StringBuilder shortyDescriptor = new StringBuilder();
     String[] argumentDescriptors = new String[arguments.length];
     shortyDescriptor.append(getShortyDescriptor(returnType));
@@ -131,10 +143,86 @@
   }
 
   private boolean isValidDescriptor(String desc) {
-    return Type.getType(desc).getDescriptor().equals(desc);
+    return getAsmType(desc).getDescriptor().equals(desc);
   }
 
   private boolean isValidInternalName(String name) {
-    return Type.getObjectType(name).getInternalName().equals(name);
+    return getAsmObjectType(name).getInternalName().equals(name);
+  }
+
+  public Type getReturnType(final String methodDescriptor) {
+    assert methodDescriptor.indexOf(')') != -1;
+    return getAsmType(methodDescriptor.substring(methodDescriptor.indexOf(')') + 1));
+  }
+
+  public int getArgumentCount(final String methodDescriptor) {
+    int charIdx = 1;
+    char c;
+    int argCount = 0;
+    while ((c = methodDescriptor.charAt(charIdx++)) != ')') {
+      if (c == 'L') {
+        while (methodDescriptor.charAt(charIdx++) != ';');
+        argCount++;
+      } else if (c != '[') {
+        argCount++;
+      }
+    }
+    return argCount;
+  }
+
+  public Type[] getArgumentTypes(final String methodDescriptor) {
+    Type[] args = new Type[getArgumentCount(methodDescriptor)];
+    int charIdx = 1;
+    char c;
+    int argIdx = 0;
+    int startType;
+    while ((c = methodDescriptor.charAt(charIdx)) != ')') {
+      switch (c) {
+        case 'V':
+          throw new Unreachable();
+        case 'Z':
+          args[argIdx++] = Type.BOOLEAN_TYPE;
+          break;
+        case 'C':
+          args[argIdx++] = Type.CHAR_TYPE;
+          break;
+        case 'B':
+          args[argIdx++] = Type.BYTE_TYPE;
+          break;
+        case 'S':
+          args[argIdx++] = Type.SHORT_TYPE;
+          break;
+        case 'I':
+          args[argIdx++] = Type.INT_TYPE;
+          break;
+        case 'F':
+          args[argIdx++] = Type.FLOAT_TYPE;
+          break;
+        case 'J':
+          args[argIdx++] = Type.LONG_TYPE;
+          break;
+        case 'D':
+          args[argIdx++] = Type.DOUBLE_TYPE;
+          break;
+        case '[':
+          startType = charIdx;
+          while (methodDescriptor.charAt(++charIdx) == '[') {
+          }
+          if (methodDescriptor.charAt(charIdx) == 'L') {
+            while (methodDescriptor.charAt(++charIdx) != ';');
+          }
+          args[argIdx++] = getAsmType(methodDescriptor.substring(startType, charIdx + 1));
+          break;
+        case 'L':
+          startType = charIdx;
+          while (methodDescriptor.charAt(++charIdx) != ';');
+          args[argIdx++] = getAsmType(methodDescriptor.substring(startType, charIdx + 1));
+          break;
+        default:
+          throw new Unreachable();
+      }
+      charIdx++;
+    }
+    return args;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index a9832dc..7976085 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -210,7 +210,7 @@
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String desc, String signature, String[] exceptions) {
-      return new CreateMethodVisitor(access, name, desc, signature, exceptions, this);
+      return new CreateMethodVisitor(application, access, name, desc, signature, exceptions, this);
     }
 
     @Override
@@ -406,14 +406,14 @@
     private List<DexValue> parameterNames = null;
     private List<DexValue> parameterFlags = null;
 
-    public CreateMethodVisitor(int access, String name, String desc, String signature,
-        String[] exceptions, CreateDexClassVisitor parent) {
+    public CreateMethodVisitor(JarApplicationReader application, int access, String name,
+        String desc, String signature, String[] exceptions, CreateDexClassVisitor parent) {
       super(ASM6);
       this.access = access;
       this.name = name;
       this.desc = desc;
       this.parent = parent;
-      parameterCount = Type.getArgumentTypes(desc).length;
+      parameterCount = application.getArgumentCount(desc);
       if (exceptions != null && exceptions.length > 0) {
         DexValue[] values = new DexValue[exceptions.length];
         for (int i = 0; i < exceptions.length; i++) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstString.java b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
index 1cfd2ae..a3a730d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstString.java
@@ -103,7 +103,7 @@
   @Override
   public boolean canBeDeadCode(IRCode code, InternalOptions options) {
     // No side-effect, such as throwing an exception, in CF.
-    return options.outputClassFiles || !instructionInstanceCanThrow();
+    return options.isGeneratingClassFiles() || !instructionInstanceCanThrow();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/MoveException.java b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
index 6d399fc..15245db 100644
--- a/src/main/java/com/android/tools/r8/ir/code/MoveException.java
+++ b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
@@ -67,7 +67,7 @@
 
   @Override
   public boolean canBeDeadCode(IRCode code, InternalOptions options) {
-    return !options.debug && !options.outputClassFiles;
+    return !options.debug && options.isGeneratingDex();
   }
 
   @Override
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 7ab9696..9004a44 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
@@ -609,9 +609,10 @@
   }
 
   private void finalizeIR(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
-    if (options.outputClassFiles) {
+    if (options.isGeneratingClassFiles()) {
       finalizeToCf(method, code, feedback);
     } else {
+      assert options.isGeneratingDex();
       finalizeToDex(method, code, feedback);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index 700d9da..157e818 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -197,7 +197,7 @@
     this.method = method;
     this.clazz = clazz;
     this.callerPosition = callerPosition;
-    parameterTypes = Arrays.asList(Type.getArgumentTypes(node.desc));
+    parameterTypes = Arrays.asList(application.getArgumentTypes(node.desc));
     state = new JarState(node.maxLocals, node.localVariables, this, application);
     AbstractInsnNode first = node.instructions.getFirst();
     initialLabel = first instanceof LabelNode ? (LabelNode) first : null;
@@ -263,7 +263,7 @@
       LocalVariableNode local = (LocalVariableNode) o;
       Type localType;
       ValueType localValueType;
-      switch (Type.getType(local.desc).getSort()) {
+      switch (application.getAsmType(local.desc).getSort()) {
         case Type.OBJECT:
         case Type.ARRAY: {
           localType = JarState.NULL_TYPE;
@@ -331,7 +331,7 @@
 
     if (isSynchronized()) {
       generatingMethodSynchronization = true;
-      Type clazzType = Type.getType(clazz.toDescriptorString());
+      Type clazzType = application.getAsmType(clazz.toDescriptorString());
       int monitorRegister;
       if (isStatic()) {
         // Load the class using a temporary on the stack.
@@ -354,7 +354,7 @@
   private void buildArgumentInstructions(IRBuilder builder) {
     int argumentRegister = 0;
     if (!isStatic()) {
-      Type thisType = Type.getType(clazz.descriptor.toString());
+      Type thisType = application.getAsmType(clazz.descriptor.toString());
       Slot slot = state.readLocal(argumentRegister++, thisType);
       builder.addThisArgument(slot.register);
     }
@@ -375,7 +375,7 @@
         new Int2ReferenceOpenHashMap<>(node.localVariables.size());
     int argumentRegister = 0;
     if (!isStatic()) {
-      Type thisType = Type.getType(clazz.descriptor.toString());
+      Type thisType = application.getAsmType(clazz.descriptor.toString());
       int register = state.writeLocal(argumentRegister++, thisType);
       initializedLocals.put(register, valueType(thisType));
     }
@@ -878,8 +878,8 @@
     }
   }
 
-  private static MemberType memberType(String fieldDesc) {
-    return memberType(Type.getType(fieldDesc));
+  private MemberType memberType(String fieldDesc) {
+    return memberType(application.getAsmType(fieldDesc));
   }
 
   private static NumericType numericType(Type type) {
@@ -927,8 +927,8 @@
     }
   }
 
-  private static Type makeArrayType(Type elementType) {
-    return Type.getObjectType("[" + elementType.getDescriptor());
+  private Type makeArrayType(Type elementType) {
+    return application.getAsmObjectType("[" + elementType.getDescriptor());
   }
 
   private static String arrayTypeDesc(int arrayTypeCode) {
@@ -1205,7 +1205,7 @@
       }
       case Opcodes.POP: {
         Slot value = state.pop();
-        assert value.isCategory1();
+        value.isCategory1();
         break;
       }
       case Opcodes.POP2: {
@@ -1539,7 +1539,7 @@
       }
       case Opcodes.NEWARRAY: {
         String desc = arrayTypeDesc(insn.operand);
-        Type type = Type.getType(desc);
+        Type type = application.getAsmType(desc);
         state.pop();
         state.push(type);
         break;
@@ -1594,7 +1594,7 @@
   }
 
   private void updateState(TypeInsnNode insn) {
-    Type type = Type.getObjectType(insn.desc);
+    Type type = application.getAsmObjectType(insn.desc);
     switch (insn.getOpcode()) {
       case Opcodes.NEW: {
         state.push(type);
@@ -1624,7 +1624,7 @@
   }
 
   private void updateState(FieldInsnNode insn) {
-    Type type = Type.getType(insn.desc);
+    Type type = application.getAsmType(insn.desc);
     switch (insn.getOpcode()) {
       case Opcodes.GETSTATIC:
         state.push(type);
@@ -1657,14 +1657,13 @@
 
   private void updateStateForInvoke(String desc, boolean implicitReceiver) {
     // Pop arguments.
-    Type[] parameterTypes = Type.getArgumentTypes(desc);
-    state.popReverse(parameterTypes.length);
+    state.popReverse(application.getArgumentCount(desc));
     // Pop implicit receiver if needed.
     if (implicitReceiver) {
       state.pop();
     }
     // Push return value if needed.
-    Type returnType = Type.getReturnType(desc);
+    Type returnType = application.getReturnType(desc);
     if (returnType != Type.VOID_TYPE) {
       state.push(returnType);
     }
@@ -1747,7 +1746,7 @@
 
   private void updateState(MultiANewArrayInsnNode insn) {
     // Type of the full array.
-    Type arrayType = Type.getObjectType(insn.desc);
+    Type arrayType = application.getAsmObjectType(insn.desc);
     state.popReverse(insn.dims, Type.INT_TYPE);
     state.push(arrayType);
   }
@@ -2368,7 +2367,7 @@
       }
       case Opcodes.NEWARRAY: {
         String desc = arrayTypeDesc(insn.operand);
-        Type type = Type.getType(desc);
+        Type type = application.getAsmType(desc);
         DexType dexType = application.getTypeFromDescriptor(desc);
         int count = state.pop(Type.INT_TYPE).register;
         int array = state.push(type);
@@ -2423,7 +2422,7 @@
   }
 
   private void build(TypeInsnNode insn, IRBuilder builder) {
-    Type type = Type.getObjectType(insn.desc);
+    Type type = application.getAsmObjectType(insn.desc);
     switch (insn.getOpcode()) {
       case Opcodes.NEW: {
         DexType dexType = application.getTypeFromName(insn.desc);
@@ -2461,7 +2460,7 @@
 
   private void build(FieldInsnNode insn, IRBuilder builder) {
     DexField field = application.getField(insn.owner, insn.name, insn.desc);
-    Type type = Type.getType(insn.desc);
+    Type type = application.getAsmType(insn.desc);
     switch (insn.getOpcode()) {
       case Opcodes.GETSTATIC:
         builder.addStaticGet(state.push(type), field);
@@ -2492,7 +2491,7 @@
 
     buildInvoke(
         insn.desc,
-        Type.getObjectType(insn.owner),
+        application.getAsmObjectType(insn.owner),
         insn.getOpcode() != Opcodes.INVOKESTATIC,
         builder,
         (types, registers) -> {
@@ -2517,7 +2516,7 @@
 
     // Build the argument list of the form [owner, param1, ..., paramN].
     // The arguments are in reverse order on the stack, so we pop off the parameters here.
-    Type[] parameterTypes = Type.getArgumentTypes(methodDesc);
+    Type[] parameterTypes = application.getArgumentTypes(methodDesc);
     Slot[] parameterRegisters = state.popReverse(parameterTypes.length);
 
     List<ValueType> types = new ArrayList<>(parameterTypes.length + 1);
@@ -2537,7 +2536,7 @@
     creator.accept(types, registers);
 
     // Move the result to the "top of stack".
-    Type returnType = Type.getReturnType(methodDesc);
+    Type returnType = application.getReturnType(methodDesc);
     if (returnType != Type.VOID_TYPE) {
       builder.addMoveResult(state.push(returnType));
     }
@@ -2780,7 +2779,7 @@
 
   private void build(MultiANewArrayInsnNode insn, IRBuilder builder) throws ApiLevelException {
     // Type of the full array.
-    Type arrayType = Type.getObjectType(insn.desc);
+    Type arrayType = application.getAsmObjectType(insn.desc);
     DexType dexArrayType = application.getType(arrayType);
     // Type of the members. Can itself be of array type, eg, 'int[]' for 'new int[x][y][]'
     DexType memberType = application.getTypeFromDescriptor(insn.desc.substring(insn.dims));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index f74d60b..791c216 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -709,7 +709,7 @@
 
   // Replace result uses for methods where something is known about what is returned.
   public void rewriteMoveResult(IRCode code) {
-    if (options.outputClassFiles) {
+    if (options.isGeneratingClassFiles()) {
       return;
     }
     AppInfoWithSubtyping appInfoWithSubtyping = appInfo.withSubtyping();
@@ -739,7 +739,7 @@
                     argumentIndex)) {
                   Value argument = invoke.arguments().get(argumentIndex);
                   assert invoke.outType().compatible(argument.outType())
-                      || (!options.outputClassFiles && verifyCompatibleFromDex(invoke, argument));
+                      || (options.isGeneratingDex() && verifyCompatibleFromDex(invoke, argument));
                   invoke.outValue().replaceUsers(argument);
                   invoke.setOutValue(null);
                 }
@@ -1397,7 +1397,7 @@
    * and fill-array-data / filled-new-array.
    */
   public void simplifyArrayConstruction(IRCode code) {
-    if (options.outputClassFiles) {
+    if (options.isGeneratingClassFiles()) {
       return;
     }
     for (BasicBlock block : code.blocks) {
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 2db0673..6747809 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -284,62 +284,46 @@
 
     // For each debug position compute the set of live locals.
     boolean localsChanged = false;
-    Int2ReferenceMap<DebugLocalInfo> currentLocals = new Int2ReferenceOpenHashMap<>();
-
     LinkedList<LocalRange> openRanges = new LinkedList<>();
     Iterator<LocalRange> rangeIterator = ranges.iterator();
     LocalRange nextStartingRange = rangeIterator.next();
 
-    // Open initial (argument) ranges.
-    while (nextStartingRange != null && nextStartingRange.start == 0) {
-      currentLocals.put(nextStartingRange.register, nextStartingRange.local);
-      openRanges.add(nextStartingRange);
-      nextStartingRange = rangeIterator.hasNext() ? rangeIterator.next() : null;
-    }
-
     for (BasicBlock block : blocks) {
-      ListIterator<Instruction> instructionIterator = block.listIterator();
+      // Skip past all spill moves to obtain the instruction number of the actual first instruction.
+      InstructionListIterator instructionIterator = block.listIterator();
+      instructionIterator.nextUntil(
+          i -> !i.isArgument() && !i.isMoveException() && !isSpillInstruction(i));
+      Instruction firstInstruction = instructionIterator.previous();
+      int firstIndex = firstInstruction.getNumber();
+
       // Close ranges up-to but excluding the first instruction. Ends are exclusive but the values
-      // might be live upon entering the first instruction (if they are used by it).
-      int entryIndex = block.entry().getNumber();
-      if (block.entry().isMoveException()) {
-        // Close locals at a move exception since they close as part of the exceptional transfer.
-        entryIndex++;
-      }
-      {
-        ListIterator<LocalRange> it = openRanges.listIterator(0);
-        while (it.hasNext()) {
-          LocalRange openRange = it.next();
-          if (openRange.end < entryIndex ||
-              // Don't close the local if it is used by the entry instruction. Exclude move
-              // exception instruction that are managed in other way that should be clean.
-              (openRange.end == entryIndex
-                  && !block.entry().isMoveException()
-                  && !usesValues(openRange.value, block.entry()))) {
-            it.remove();
-            assert currentLocals.get(openRange.register) == openRange.local;
-            currentLocals.remove(openRange.register);
-          }
-        }
-      }
+      // might be live upon entering the first instruction (if they are used by it). Since we
+      // skipped move-exception this closes locals at the move exception which should close as part
+      // of the exceptional transfer.
+      openRanges.removeIf(openRange -> !isLocalLiveAtInstruction(openRange, firstInstruction));
+
       // Open ranges up-to but excluding the first instruction. Starts are inclusive but entry is
       // prior to the first instruction.
-      while (nextStartingRange != null && nextStartingRange.start < entryIndex) {
+      while (nextStartingRange != null && nextStartingRange.start < firstIndex) {
         // If the range is live at this index open it. Again the end is inclusive here because the
         // instruction is live at block entry if it is live at entry to the first instruction.
-        if (entryIndex <= nextStartingRange.end) {
+        if (isLocalLiveAtInstruction(nextStartingRange, firstInstruction)) {
           openRanges.add(nextStartingRange);
-          assert !currentLocals.containsKey(nextStartingRange.register);
-          currentLocals.put(nextStartingRange.register, nextStartingRange.local);
         }
         nextStartingRange = rangeIterator.hasNext() ? rangeIterator.next() : null;
       }
-      if (block.entry().isMoveException()) {
-        fixupLocalsLiveAtMoveException(
-            block, instructionIterator, openRanges, currentLocals, allocator);
-      } else {
-        block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(currentLocals));
+
+      // Initialize current locals (registers after any spill instructions).
+      Int2ReferenceMap<DebugLocalInfo> currentLocals =
+          new Int2ReferenceOpenHashMap<>(openRanges.size());
+      for (LocalRange openRange : openRanges) {
+        currentLocals.put(openRange.register, openRange.local);
       }
+
+      // Set locals at entry. This will adjust initial local registers in case of spilling.
+      setLocalsAtEntry(block, instructionIterator, openRanges, currentLocals, allocator);
+
+      // Iterate the block instructions and emit locals changed events.
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
         if (instruction.isDebugLocalRead()) {
@@ -388,36 +372,45 @@
     }
   }
 
+  private static boolean isLocalLiveAtInstruction(LocalRange range, Instruction instruction) {
+    int number = instruction.getNumber();
+    assert range.start < number;
+    return number < range.end || (number == range.end && usesValues(range.value, instruction));
+  }
+
   private static boolean usesValues(Value usedValue, Instruction instruction) {
     return instruction.inValues().contains(usedValue)
         || instruction.getDebugValues().contains(usedValue);
   }
 
-  private static void fixupLocalsLiveAtMoveException(
+  private static void setLocalsAtEntry(
       BasicBlock block,
-      ListIterator<Instruction> instructionIterator,
+      InstructionListIterator instructionIterator,
       List<LocalRange> openRanges,
       Int2ReferenceMap<DebugLocalInfo> finalLocals,
       RegisterAllocator allocator) {
-    Int2ReferenceMap<DebugLocalInfo> initialLocals = new Int2ReferenceOpenHashMap<>();
-    int exceptionalIndex = block.getPredecessors().get(0).exceptionalExit().getNumber();
+    // If this is the graph-entry or there are no moves entry locals are current locals.
+    if (block.getPredecessors().isEmpty() || block.entry() == instructionIterator.peekNext()) {
+      assert !block.entry().isMoveException();
+      assert !isSpillInstruction(block.entry());
+      block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(finalLocals));
+      return;
+    }
+    // Otherwise entry locals are the registers of the predecessor, ie, prior to spill instructions.
+    Int2ReferenceMap<DebugLocalInfo> initialLocals =
+        new Int2ReferenceOpenHashMap<>(openRanges.size());
+    int predecessorExitIndex =
+        block.entry().isMoveException()
+            ? block.getPredecessors().get(0).exceptionalExit().getNumber()
+            : block.getPredecessors().get(0).exit().getNumber();
     for (LocalRange open : openRanges) {
-      int exceptionalRegister =
-          allocator.getArgumentOrAllocateRegisterForValue(open.value, exceptionalIndex);
-      initialLocals.put(exceptionalRegister, open.local);
+      int predecessorRegister =
+          allocator.getArgumentOrAllocateRegisterForValue(open.value, predecessorExitIndex);
+      initialLocals.put(predecessorRegister, open.local);
     }
-    block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(initialLocals));
-    Instruction entry = instructionIterator.next();
-    assert block.entry() == entry;
-    assert block.entry().isMoveException();
-    // Skip past spill instructions.
-    while (instructionIterator.hasNext()) {
-      if (!isSpillInstruction(instructionIterator.next())) {
-        instructionIterator.previous();
-        break;
-      }
-    }
-    // Compute the final change in locals and insert it after the last move.
+    block.setLocalsAtEntry(initialLocals);
+
+    // Compute the final change in locals and insert it after the last spill instruction.
     Int2ReferenceMap<DebugLocalInfo> ending = new Int2ReferenceOpenHashMap<>();
     Int2ReferenceMap<DebugLocalInfo> starting = new Int2ReferenceOpenHashMap<>();
     for (Entry<DebugLocalInfo> initialLocal : initialLocals.int2ReferenceEntrySet()) {
@@ -1941,7 +1934,7 @@
       }
       intervals.addRange(new LiveRange(instructionNumber, end));
       assert unconstrainedForCf(intervals.getRegisterLimit(), options);
-      if (!options.outputClassFiles && !value.isPhi()) {
+      if (options.isGeneratingDex() && !value.isPhi()) {
         int constraint = value.definition.maxOutValueRegister();
         intervals.addUse(new LiveIntervalsUse(instructionNumber, constraint));
       }
@@ -2003,7 +1996,7 @@
                 instruction.getNumber() + INSTRUCTION_NUMBER_DELTA,
                 liveIntervals,
                 options);
-            assert !options.outputClassFiles || instruction.isArgument()
+            assert !options.isGeneratingClassFiles() || instruction.isArgument()
                 : "Arguments should be the only potentially unused local in CF";
           }
           live.remove(definition);
@@ -2015,7 +2008,7 @@
               live.add(use);
               addLiveRange(use, block, instruction.getNumber(), liveIntervals, options);
             }
-            if (!options.outputClassFiles) {
+            if (options.isGeneratingDex()) {
               int inConstraint = instruction.maxInValueRegister();
               LiveIntervals useIntervals = use.getLiveIntervals();
               useIntervals.addUse(new LiveIntervalsUse(instruction.getNumber(), inConstraint));
@@ -2037,7 +2030,7 @@
   }
 
   private static boolean unconstrainedForCf(int constraint, InternalOptions options) {
-    return !options.outputClassFiles || constraint == Constants.U16BIT_MAX;
+    return !options.isGeneratingClassFiles() || constraint == Constants.U16BIT_MAX;
   }
 
   private void clearUserInfo() {
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 88e012e..019283a 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -5,7 +5,7 @@
 
 import static org.objectweb.asm.Opcodes.ASM6;
 
-import com.android.tools.r8.OutputSink;
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexApplication;
@@ -13,11 +13,11 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.util.Collections;
 import java.util.concurrent.ExecutorService;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassWriter;
@@ -37,20 +37,20 @@
     this.options = options;
   }
 
-  public void write(OutputSink outputSink, ExecutorService executor) throws IOException {
+  public void write(ClassFileConsumer consumer, ExecutorService executor) throws IOException {
     application.timing.begin("CfApplicationWriter.write");
     try {
-      writeApplication(outputSink, executor);
+      writeApplication(consumer, executor);
     } finally {
       application.timing.end();
     }
   }
 
-  private void writeApplication(OutputSink outputSink, ExecutorService executor)
+  private void writeApplication(ClassFileConsumer consumer, ExecutorService executor)
       throws IOException {
     for (DexProgramClass clazz : application.classes()) {
       if (clazz.getSynthesizedFrom().isEmpty()) {
-        writeClass(clazz, outputSink);
+        writeClass(clazz, consumer);
       } else {
         throw new Unimplemented("No support for synthetics in the Java bytecode backend.");
       }
@@ -62,7 +62,7 @@
     return version > 49 ? 49 : version;
   }
 
-  private void writeClass(DexProgramClass clazz, OutputSink outputSink) throws IOException {
+  private void writeClass(DexProgramClass clazz, ClassFileConsumer consumer) throws IOException {
     ClassWriter writer = new ClassWriter(0);
     writer.visitSource(clazz.sourceFile != null ? clazz.sourceFile.toString() : null, null);
     int version = downgrade(clazz.getClassFileVersion());
@@ -102,7 +102,8 @@
 
     byte[] result = writer.toByteArray();
     assert verifyCf(result);
-    outputSink.writeClassFile(result, Collections.singleton(desc), desc);
+    ExceptionUtils.withConsumeResourceHandler(
+        options.reporter, handler -> consumer.accept(result, desc, handler));
   }
 
   private Object getStaticValue(DexEncodedField field) {
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 9daffb0..23a92a0 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
+import com.android.tools.r8.utils.IdentifierUtils;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.util.HashMap;
@@ -78,6 +79,10 @@
         : '\n';
   }
 
+  private boolean hasNext() {
+    return lineOffset < line.length();
+  }
+
   private char next() {
     try {
       return line.charAt(lineOffset++);
@@ -129,6 +134,9 @@
   }
 
   private char expect(char c) {
+    if (!hasNext()) {
+      throw new ParseException("Expected '" + c + "'", true);
+    }
     if (next() != c) {
       throw new ParseException("Expected '" + c + "'");
     }
@@ -151,14 +159,19 @@
       String before = parseType(false);
       skipWhitespace();
       // Workaround for proguard map files that contain entries for package-info.java files.
-      if (!acceptArrow()) {
-        // If this was a package-info line, we parsed the "package" string.
-        if (!before.endsWith("package") || !acceptString("-info")) {
-          throw new ParseException("Expected arrow after class name " + before);
-        }
+      assert IdentifierUtils.isDexIdentifierPart('-');
+      if (before.endsWith("package-info")) {
         skipLine();
         continue;
       }
+      if (before.endsWith("-") && acceptString(">")) {
+        // With - as a legal identifier part the grammar is ambiguous, and we treat a->b as a -> b,
+        // and not as a- > b (which would be a parse error).
+        before = before.substring(0, before.length() - 1);
+      } else {
+        skipWhitespace();
+        acceptArrow();
+      }
       skipWhitespace();
       String after = parseType(false);
       expect(':');
@@ -286,17 +299,17 @@
       next();
       isInit = true;
     }
-    if (!Character.isJavaIdentifierStart(peek())) {
+    if (!IdentifierUtils.isDexIdentifierStart(peek())) {
       throw new ParseException("Identifier expected");
     }
     next();
-    while (Character.isJavaIdentifierPart(peek())) {
+    while (IdentifierUtils.isDexIdentifierPart(peek())) {
       next();
     }
     if (isInit) {
       expect('>');
     }
-    if (Character.isJavaIdentifierPart(peek())) {
+    if (IdentifierUtils.isDexIdentifierPart(peek())) {
       throw new ParseException("End of identifier expected");
     }
   }
@@ -422,17 +435,27 @@
 
     private final int lineNo;
     private final int lineOffset;
+    private final boolean eol;
     private final String msg;
 
     ParseException(String msg) {
+      this(msg, false);
+    }
+
+    ParseException(String msg, boolean eol) {
       lineNo = ProguardMapReader.this.lineNo;
       lineOffset = ProguardMapReader.this.lineOffset;
+      this.eol = eol;
       this.msg = msg;
     }
 
     @Override
     public String toString() {
-      return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
+      if (eol) {
+        return "Parse error [" + lineNo + ":eol] " + msg;
+      } else {
+        return "Parse error [" + lineNo + ":" + lineOffset + "] " + msg;
+      }
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
index 23580a5..ee771ff 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidAppOutputSink.java
@@ -103,20 +103,6 @@
   }
 
   @Override
-  public synchronized void writeClassFile(
-      byte[] contents, Set<String> classDescriptors, String primaryClassName) throws IOException {
-    assert dexFilesWithPrimary.isEmpty() && dexFilesWithId.isEmpty();
-    classFiles.add(new DescriptorsWithContents(classDescriptors, contents));
-    super.writeClassFile(contents, classDescriptors, primaryClassName);
-  }
-
-  @Override
-  public void writeProguardSeedsFile(byte[] contents) throws IOException {
-    builder.setProguardSeedsData(contents);
-    super.writeProguardSeedsFile(contents);
-  }
-
-  @Override
   public void close() throws IOException {
     assert !closed;
     if (!dexFilesWithPrimary.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
index 0bca98a..74b1b21 100644
--- a/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryOutputSink.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
 
 import java.io.IOException;
@@ -49,12 +48,6 @@
     writeFileFromDescriptor(contents, primaryClassName, DEX_EXTENSION);
   }
 
-  @Override
-  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    writeFileFromDescriptor(contents, primaryClassName, CLASS_EXTENSION);
-  }
-
   private void writeFileFromDescriptor(byte[] contents, String descriptor, String extension)
       throws IOException {
     Path target = outputDirectory.resolve(getOutputFileName(descriptor, extension));
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
index 2b7d907..80d08ff 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
@@ -3,14 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.StringConsumer;
+import java.util.function.Consumer;
 
 public abstract class ExceptionUtils {
 
   public static void withConsumeResourceHandler(
-      Reporter reporter, String data, StringConsumer consumer) {
+      Reporter reporter, StringConsumer consumer, String data) {
+    withConsumeResourceHandler(reporter, handler -> consumer.accept(data, handler));
+  }
+
+  public static void withConsumeResourceHandler(
+      Reporter reporter, Consumer<DiagnosticsHandler> consumer) {
     // Unchecked exceptions simply propagate out, aborting the compilation forcefully.
-    consumer.accept(data, reporter);
+    consumer.accept(reporter);
     // Fail fast for now. We might consider delaying failure since consumer failure does not affect
     // the compilation. We might need to be careful to correctly identify errors so as to exit
     // compilation with an error code.
diff --git a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
index 6e5b33f..b2699fc 100644
--- a/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/FileSystemOutputSink.java
@@ -25,7 +25,6 @@
   }
 
   String getOutputFileName(int index) {
-    assert !options.outputClassFiles;
     return index == 0 ? "classes.dex" : ("classes" + (index + 1) + FileUtils.DEX_EXTENSION);
   }
 
@@ -34,11 +33,6 @@
     return DescriptorUtils.getClassBinaryNameFromDescriptor(classDescriptor) + extension;
   }
 
-  @Override
-  public void writeProguardSeedsFile(byte[] contents) throws IOException {
-    FileUtils.writeToFile(options.proguardConfiguration.getSeedFile(), System.out, contents);
-  }
-
   protected OutputMode getOutputMode() {
     return options.outputMode;
   }
diff --git a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
index a6fb545..71e48cf 100644
--- a/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/ForwardingOutputSink.java
@@ -33,17 +33,6 @@
   }
 
   @Override
-  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    forwardTo.writeClassFile(contents, classDescriptors, primaryClassName);
-  }
-
-  @Override
-  public void writeProguardSeedsFile(byte[] contents) throws IOException {
-    forwardTo.writeProguardSeedsFile(contents);
-  }
-
-  @Override
   public void close() throws IOException {
     forwardTo.close();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
new file mode 100644
index 0000000..3097a07
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/IdentifierUtils.java
@@ -0,0 +1,47 @@
+// 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.utils;
+
+public class IdentifierUtils {
+  public static boolean isDexIdentifierStart(char ch) {
+    // Dex does not have special restrictions on the first char of an identifier.
+    return isDexIdentifierPart(ch);
+  }
+
+  public static boolean isDexIdentifierPart(char ch) {
+    return isSimpleNameChar(ch);
+  }
+
+  private static boolean isSimpleNameChar(char ch) {
+    if (ch >= 'A' && ch <= 'Z') {
+      return true;
+    }
+    if (ch >= 'a' && ch <= 'z') {
+      return true;
+    }
+    if (ch >= '0' && ch <= '9') {
+      return true;
+    }
+    if (ch == '$' || ch == '-' || ch == '_') {
+      return true;
+    }
+    if (ch >= 0x00a1 && ch <= 0x1fff) {
+      return true;
+    }
+    if (ch >= 0x2010 && ch <= 0x2027) {
+      return true;
+    }
+    if (ch >= 0x2030 && ch <= 0xd7ff) {
+      return true;
+    }
+    if (ch >= 0xe000 && ch <= 0xffef) {
+      return true;
+    }
+    if (ch >= 0x10000 && ch <= 0x10ffff) {
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
index 21f98f6..76f03bd 100644
--- a/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/IgnoreContentsOutputSink.java
@@ -20,17 +20,6 @@
   }
 
   @Override
-  public void writeClassFile(
-      byte[] contents, Set<String> classDescriptors, String primaryClassName) {
-    // Intentionally left empty.
-  }
-
-  @Override
-  public void writeProguardSeedsFile(byte[] contents) {
-    // Intentionally left empty.
-  }
-
-  @Override
   public void close() throws IOException {
     // Intentionally left empty.
   }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index dd9f970..9ae315f 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
@@ -35,6 +37,9 @@
   public final ProguardConfiguration proguardConfiguration;
   public final Reporter reporter;
 
+  // TODO(zerny): Make this private-final once we have full program-consumer support.
+  public ProgramConsumer programConsumer = null;
+
   // Constructor for testing and/or other utilities.
   public InternalOptions() {
     reporter = new Reporter(new DefaultDiagnosticsHandler());
@@ -62,8 +67,6 @@
 
   public boolean printTimes = false;
 
-  public boolean outputClassFiles = false;
-
   // Optimization-related flags. These should conform to -dontoptimize.
   public boolean skipDebugLineNumberOpt = false;
   public boolean skipClassMerging = true;
@@ -99,6 +102,24 @@
     return marker;
   }
 
+  public boolean isGeneratingDex() {
+    return programConsumer == null;
+  }
+
+  public boolean isGeneratingClassFiles() {
+    return programConsumer instanceof ClassFileConsumer;
+  }
+
+  public ClassFileConsumer getClassFileConsumer() {
+    return (ClassFileConsumer) programConsumer;
+  }
+
+  public void closeProgramConsumer() {
+    if (programConsumer != null) {
+      programConsumer.finished(reporter);
+    }
+  }
+
   public List<String> methodsFilter = ImmutableList.of();
   public int minApiLevel = AndroidApiLevel.getDefault().getLevel();
   // Skipping min_api check and compiling an intermediate result intended for later merging.
@@ -178,6 +199,10 @@
   // If non null it must be and passed to the consumer.
   public StringConsumer proguardMapConsumer = null;
 
+  // If null, no proguad seeds info needs to be computed.
+  // If non null it must be and passed to the consumer.
+  public StringConsumer proguardSeedsConsumer = null;
+
   // If null, no usage information needs to be computed.
   // If non-null, it must be and is passed to the consumer.
   public StringConsumer usageInformationConsumer = null;
diff --git a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
index ac6b341..1e1037e 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipFileOutputSink.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
 
 import java.io.IOException;
@@ -11,8 +10,6 @@
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.Set;
-import java.util.zip.CRC32;
-import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
 public class ZipFileOutputSink extends FileSystemOutputSink {
@@ -39,26 +36,11 @@
   }
 
   @Override
-  public void writeClassFile(byte[] contents, Set<String> classDescriptors, String primaryClassName)
-      throws IOException {
-    writeToZipFile(getOutputFileName(primaryClassName, CLASS_EXTENSION), contents);
-  }
-
-  @Override
   public void close() throws IOException {
     outputStream.close();
   }
 
   private synchronized void writeToZipFile(String outputPath, byte[] content) throws IOException {
-    CRC32 crc = new CRC32();
-    crc.update(content);
-    ZipEntry zipEntry = new ZipEntry(outputPath);
-    zipEntry.setMethod(ZipEntry.STORED);
-    zipEntry.setSize(content.length);
-    zipEntry.setCompressedSize(content.length);
-    zipEntry.setCrc(crc.getValue());
-    outputStream.putNextEntry(zipEntry);
-    outputStream.write(content);
-    outputStream.closeEntry();
+    ZipUtils.writeToZipStream(outputStream, outputPath, content);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 76d84c2..8f46f3e 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -15,8 +15,10 @@
 import java.util.Enumeration;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.zip.CRC32;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
 
 public class ZipUtils {
 
@@ -62,4 +64,18 @@
       });
     return outFiles;
   }
+
+  public static void writeToZipStream(ZipOutputStream stream, String entry, byte[] content)
+      throws IOException {
+    CRC32 crc = new CRC32();
+    crc.update(content);
+    ZipEntry zipEntry = new ZipEntry(entry);
+    zipEntry.setMethod(ZipEntry.STORED);
+    zipEntry.setSize(content.length);
+    zipEntry.setCompressedSize(content.length);
+    zipEntry.setCrc(crc.getValue());
+    stream.putNextEntry(zipEntry);
+    stream.write(content);
+    stream.closeEntry();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index d3b5c14..2eb37a6 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.google.common.collect.ImmutableList;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -47,7 +46,7 @@
 
   protected ProcessResult runOnJava(String main, byte[]... classes) throws IOException {
     Path file = writeToZip(classes);
-    return ToolHelper.runJavaNoVerify(ImmutableList.of(file.toString()), main);
+    return ToolHelper.runJavaNoVerify(file, main);
   }
 
   private Path writeToZip(byte[]... classes) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 4b042fe..0963631 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.errors.Unreachable;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
@@ -257,10 +256,14 @@
       case R8: {
         ToolHelper.runR8(R8Command.builder()
             .addProgramFiles(getInputFile())
-            .setOutputPath(getOutputFile())
+            .setOutputPath(output == Output.CF ? null : getOutputFile())
             .setMode(mode)
             .build(),
-            options -> options.outputClassFiles = (output == Output.CF));
+            options -> {
+              if (output == Output.CF) {
+                options.programConsumer = new ClassFileConsumer.ArchiveConsumer(getOutputFile());
+              }
+            });
         break;
       }
       default:
@@ -277,8 +280,7 @@
     String original = getOriginalDexFile().toString();
     Path generated = getOutputFile();
 
-    ToolHelper.ProcessResult javaResult =
-        ToolHelper.runJava(ImmutableList.of(getOriginalJarFile("").toString()), mainClass);
+    ToolHelper.ProcessResult javaResult = ToolHelper.runJava(getOriginalJarFile(""), mainClass);
     if (javaResult.exitCode != 0) {
       System.out.println(javaResult.stdout);
       System.err.println(javaResult.stderr);
@@ -295,8 +297,7 @@
     }
 
     if (output == Output.CF) {
-      ToolHelper.ProcessResult result =
-          ToolHelper.runJava(ImmutableList.of(generated.toString()), mainClass);
+      ToolHelper.ProcessResult result = ToolHelper.runJava(generated, mainClass);
       if (result.exitCode != 0) {
         System.err.println(result.stderr);
         fail("JVM failed on compiled output for: " + mainClass);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
index fd29dc1..bb8c686 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
@@ -11,8 +11,6 @@
 
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
@@ -87,8 +85,7 @@
       }
       String output = ToolHelper.runArtNoVerificationErrors(out.toString(), qualifiedMainClass);
       if (!expectedToFail) {
-        ProcessResult javaResult =
-            ToolHelper.runJava(ImmutableList.of(inputFile.toString()), qualifiedMainClass);
+        ProcessResult javaResult = ToolHelper.runJava(inputFile, qualifiedMainClass);
         assertEquals("JVM run failed", javaResult.exitCode, 0);
         assertTrue(
             "JVM output does not match art output.\n\tjvm: "
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 8caff67..3bd3086 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -397,9 +397,7 @@
         null);
     if (!expectedToFail && !skipRunningOnJvm(testName)) {
       ToolHelper.ProcessResult javaResult =
-          ToolHelper.runJava(
-              Arrays.stream(jars).map(path -> path.toString()).collect(Collectors.toList()),
-              qualifiedMainClass);
+          ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
       assertTrue(
           "JVM output does not match art output.\n\tjvm: "
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index 4ff65ba..c8f50fa 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -244,9 +244,7 @@
         null);
     if (!expectedToFail) {
       ToolHelper.ProcessResult javaResult =
-          ToolHelper.runJava(
-              Arrays.stream(jars).map(path -> path.toString()).collect(Collectors.toList()),
-              qualifiedMainClass);
+          ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
       assertTrue(
           "JVM output does not match art output.\n\tjvm: "
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 37a1697..afa0f67 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -246,9 +246,7 @@
         null);
     if (!expectedToFail) {
       ToolHelper.ProcessResult javaResult =
-          ToolHelper.runJava(
-              Arrays.stream(jars).map(path -> path.toString()).collect(Collectors.toList()),
-              qualifiedMainClass);
+          ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
       assertEquals("JVM run failed", javaResult.exitCode, 0);
       assertTrue(
           "JVM output does not match art output.\n\tjvm: "
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 208a8f9..46a769d 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -783,25 +783,35 @@
   public static ProcessResult runJava(Class clazz) throws Exception {
     String main = clazz.getCanonicalName();
     Path path = getClassPathForTests();
-    return runJava(ImmutableList.of(path.toString()), main);
+    return runJava(path, main);
   }
 
   public static ProcessResult runJavaNoVerify(Class clazz) throws Exception {
     String main = clazz.getCanonicalName();
     Path path = getClassPathForTests();
-    return runJavaNoVerify(ImmutableList.of(path.toString()), main);
+    return runJavaNoVerify(path, main);
   }
 
-  public static ProcessResult runJava(List<String> classpath, String mainClass) throws IOException {
-    ProcessBuilder builder = new ProcessBuilder(
-        getJavaExecutable(), "-cp", String.join(PATH_SEPARATOR, classpath), mainClass);
+  public static ProcessResult runJava(Path classpath, String mainClass) throws IOException {
+    return runJava(ImmutableList.of(classpath), mainClass);
+  }
+
+  public static ProcessResult runJava(List<Path> classpath, String mainClass) throws IOException {
+    String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
+    ProcessBuilder builder = new ProcessBuilder(getJavaExecutable(), "-cp", cp, mainClass);
     return runProcess(builder);
   }
 
-  public static ProcessResult runJavaNoVerify(List<String> classpath, String mainClass)
+  public static ProcessResult runJavaNoVerify(Path classpath, String mainClass)
       throws IOException {
+    return runJavaNoVerify(ImmutableList.of(classpath), mainClass);
+  }
+
+  public static ProcessResult runJavaNoVerify(List<Path> classpath, String mainClass)
+      throws IOException {
+    String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
     ProcessBuilder builder = new ProcessBuilder(
-        getJavaExecutable(), "-cp", String.join(PATH_SEPARATOR, classpath), "-noverify", mainClass);
+        getJavaExecutable(), "-cp", cp, "-noverify", mainClass);
     return runProcess(builder);
   }
 
diff --git a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
index 20ce22d..46b930d 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugStreamComparator.java
@@ -84,25 +84,24 @@
         }
       } catch (AssertionError e) {
         for (int i = 0; i < names.size(); i++) {
-          print(names.get(i), states.get(i));
+          System.err.println(names.get(i) + ": " + prettyPrintState(states.get(i)));
         }
         throw e;
       }
     }
   }
 
-  private void print(String name, DebuggeeState state) {
-    System.err.println(
-        name
-            + ": "
-            + state.getSourceFile()
-            + ":"
-            + state.getLineNumber()
-            + " "
-            + state.getClassName()
-            + "."
-            + state.getMethodName()
-            + state.getMethodSignature());
+  public static String prettyPrintState(DebuggeeState state) {
+    StringBuilder builder = new StringBuilder()
+        .append(state.getSourceFile())
+        .append(':')
+        .append(state.getLineNumber())
+        .append(' ')
+        .append(state.getClassName())
+        .append('.')
+        .append(state.getMethodName())
+        .append(state.getMethodSignature());
+    return builder.toString();
   }
 
   private void verifyStatesEqual(List<DebuggeeState> states) {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index e81f9b3..0878a30 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
@@ -147,9 +148,32 @@
     new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper).runBare();
   }
 
-  /** Lazily debug-step an execution producing a stream of successive debuggee states. */
+  /**
+   *  Lazily debug-step an execution starting from main(String[]) in {@code debuggeeClass}.
+   *
+   *  @return A stream of successive debuggee states.
+   *  */
   public Stream<JUnit3Wrapper.DebuggeeState> streamDebugTest(
       DebugTestConfig config, String debuggeeClass, StepFilter filter) throws Throwable {
+    return streamDebugTest(
+        config,
+        debuggeeClass,
+        breakpoint(debuggeeClass, "main", "([Ljava/lang/String;)V"),
+        filter);
+  }
+
+  /**
+   * Lazily debug-step an execution starting from {@code breakpoint}.
+   *
+   * @return A stream of successive debuggee states.
+   */
+  public Stream<JUnit3Wrapper.DebuggeeState> streamDebugTest(
+      DebugTestConfig config,
+      String debuggeeClass,
+      JUnit3Wrapper.Command breakpoint,
+      StepFilter filter)
+      throws Throwable {
+    assert breakpoint instanceof JUnit3Wrapper.Command.BreakpointCommand;
 
     // Continuous single-step command.
     // The execution of the command pushes itself onto the command queue ensuring the next step.
@@ -171,7 +195,7 @@
         new JUnit3Wrapper(
             config,
             debuggeeClass,
-            ImmutableList.of(breakpoint(debuggeeClass, "main", "([Ljava/lang/String;)V"), run()),
+            ImmutableList.of(breakpoint, run()),
             null);
 
     // Setup the initial state for the JDWP test base and run the program to the initial breakpoint.
@@ -822,6 +846,22 @@
       Exit
     }
 
+    // We expect to have either a single event or two events with one being an installed breakpoint.
+    private ParsedEvent getPrimaryEvent(ParsedEvent[] events) {
+      assertTrue(events.length == 1 || events.length == 2);
+      if (events.length == 1) {
+        return events[0];
+      }
+      assertEquals(2, events.length);
+      for (ParsedEvent event : events) {
+        if (event.getEventKind() == EventKind.BREAKPOINT) {
+          return event;
+        }
+      }
+      fail("Expected breakpoint when receiving multiple events.");
+      throw new Unreachable();
+    }
+
     private void processEvents() {
       EventPacket eventPacket = getMirror().receiveEvent();
       ParsedEvent[] parsedEvents = ParsedEvent.parseEventPacket(eventPacket);
@@ -837,9 +877,7 @@
           logWriter.println(msg);
         }
       }
-      // We only expect one event at a time.
-      assertEquals(1, parsedEvents.length);
-      ParsedEvent parsedEvent = parsedEvents[0];
+      ParsedEvent parsedEvent = getPrimaryEvent(parsedEvents);
       byte eventKind = parsedEvent.getEventKind();
       int requestID = parsedEvent.getRequestID();
 
diff --git a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
index 02403f6..e5e3c83 100644
--- a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debug;
 
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
@@ -35,11 +36,10 @@
     Path output = temp.newFolder().toPath().resolve("r8_debug_cf_output.jar");
     ToolHelper.runR8(
         R8Command.builder()
-            .setOutputPath(output)
             .addProgramFiles(input)
             .setMode(CompilationMode.DEBUG)
             .build(),
-        options -> options.outputClassFiles = true);
+        options -> options.programConsumer = new ClassFileConsumer.ArchiveConsumer(output));
     return new CfDebugTestConfig(output);
   }
 
diff --git a/src/test/java/com/android/tools/r8/debug/LocalsTest.java b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
index 9c8b8fe..3f59cef 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalsTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
@@ -741,9 +741,11 @@
     final String methodName = "localVisibilityIntoLoop";
 
     List<Command> commands = new ArrayList<>();
-    commands.add(breakpoint(className, methodName, 359));
+    commands.add(breakpoint(className, methodName, 358));
     commands.add(run());
     commands.add(checkMethod(className, methodName));
+    commands.add(checkLine(SOURCE_FILE, 358));
+    commands.add(stepOver());
     commands.add(checkLine(SOURCE_FILE, 359));
     commands.add(checkNoLocal("Ai"));
     commands.add(checkNoLocal("Bi"));
@@ -766,7 +768,9 @@
     commands.add(checkLocal("Ai"));
     commands.add(checkLocal("Bi"));
     commands.add(checkLocal("i", Value.createInt(0)));
-    commands.add(run());
+    commands.add(stepOver());
+    commands.add(stepOver());
+    commands.add(stepOver());
     commands.add(checkMethod(className, methodName));
     commands.add(checkLine(SOURCE_FILE, 359));
     commands.add(checkNoLocal("Ai"));
diff --git a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
index 757dd63..ff95ab388 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationException;
@@ -16,6 +17,8 @@
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
@@ -23,6 +26,9 @@
 import com.android.tools.r8.utils.ArtErrorParser.ArtErrorInfo;
 import com.android.tools.r8.utils.ArtErrorParser.ArtErrorParserException;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundFieldSubject;
+import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.OutputMode;
@@ -34,8 +40,11 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import org.junit.ComparisonFailure;
 import org.junit.Rule;
@@ -173,4 +182,143 @@
       }
     }
   }
+
+  public void assertIdenticalApplicationsUpToCode(
+      AndroidApp app1, AndroidApp app2, boolean allowNewClassesInApp2)
+      throws IOException, ExecutionException {
+    DexInspector inspect1 = new DexInspector(app1);
+    DexInspector inspect2 = new DexInspector(app2);
+
+    class Pair<T> {
+      private T first;
+      private T second;
+
+      private void set(boolean selectFirst, T value) {
+        if (selectFirst) {
+          first = value;
+        } else {
+          second = value;
+        }
+      }
+    }
+
+    // Collect all classes from both inspectors, indexed by finalDescriptor.
+    Map<String, Pair<FoundClassSubject>> allClasses = new HashMap<>();
+
+    BiConsumer<DexInspector, Boolean> collectClasses =
+        (inspector, selectFirst) -> {
+          inspector.forAllClasses(
+              clazz -> {
+                String finalDescriptor = clazz.getFinalDescriptor();
+                allClasses.compute(
+                    finalDescriptor,
+                    (k, v) -> {
+                      if (v == null) {
+                        v = new Pair<>();
+                      }
+                      v.set(selectFirst, clazz);
+                      return v;
+                    });
+              });
+        };
+
+    collectClasses.accept(inspect1, true);
+    collectClasses.accept(inspect2, false);
+
+    for (Map.Entry<String, Pair<FoundClassSubject>> classEntry : allClasses.entrySet()) {
+      String className = classEntry.getKey();
+      FoundClassSubject class1 = classEntry.getValue().first;
+      FoundClassSubject class2 = classEntry.getValue().second;
+
+      assert class1 != null || class2 != null;
+
+      if (!allowNewClassesInApp2) {
+        assertNotNull(String.format("Class %s is missing from the first app.", className), class1);
+      }
+      assertNotNull(String.format("Class %s is missing from the second app.", className), class2);
+
+      if (class1 == null) {
+        continue;
+      }
+
+      // Collect all methods for this class from both apps.
+      Map<MethodSignature, Pair<FoundMethodSubject>> allMethods = new HashMap<>();
+
+      BiConsumer<FoundClassSubject, Boolean> collectMethods =
+          (classSubject, selectFirst) -> {
+            classSubject.forAllMethods(
+                m -> {
+                  MethodSignature fs = m.getFinalSignature();
+                  allMethods.compute(
+                      fs,
+                      (k, v) -> {
+                        if (v == null) {
+                          v = new Pair<>();
+                        }
+                        v.set(selectFirst, m);
+                        return v;
+                      });
+                });
+          };
+
+      collectMethods.accept(class1, true);
+      collectMethods.accept(class2, false);
+
+      for (Map.Entry<MethodSignature, Pair<FoundMethodSubject>> methodEntry :
+          allMethods.entrySet()) {
+        MethodSignature signature = methodEntry.getKey();
+        FoundMethodSubject method1 = methodEntry.getValue().first;
+        FoundMethodSubject method2 = methodEntry.getValue().second;
+        assert method1 != null || method2 != null;
+
+        assertNotNull(
+            String.format(
+                "Method %s of class %s is missing from the first app.", signature, className),
+            method1);
+        assertNotNull(
+            String.format(
+                "Method %s of class %s is missing from the second app.", signature, className),
+            method2);
+      }
+
+      // Collect all fields for this class from both apps.
+      Map<FieldSignature, Pair<FoundFieldSubject>> allFields = new HashMap<>();
+
+      BiConsumer<FoundClassSubject, Boolean> collectFields =
+          (classSubject, selectFirst) -> {
+            classSubject.forAllFields(
+                f -> {
+                  FieldSignature fs = f.getFinalSignature();
+                  allFields.compute(
+                      fs,
+                      (k, v) -> {
+                        if (v == null) {
+                          v = new Pair<>();
+                        }
+                        v.set(selectFirst, f);
+                        return v;
+                      });
+                });
+          };
+
+      collectFields.accept(class1, true);
+      collectFields.accept(class2, false);
+
+      for (Map.Entry<FieldSignature, Pair<FoundFieldSubject>> fieldEntry : allFields.entrySet()) {
+        FieldSignature signature = fieldEntry.getKey();
+        FoundFieldSubject field1 = fieldEntry.getValue().first;
+        FoundFieldSubject field2 = fieldEntry.getValue().second;
+        assert field1 != null || field2 != null;
+
+        assertNotNull(
+            String.format(
+                "Field %s of class %s is missing from the first app.", signature, className),
+            field1);
+        assertNotNull(
+            String.format(
+                "Field %s of class %s is missing from the second app.", signature, className),
+            field2);
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
index cb3d810..6fd6e1b 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.internal;
 
-import static org.junit.Assert.assertEquals;
-
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.ToolHelper;
@@ -45,9 +43,6 @@
               options.proguardMapConsumer = StringConsumer.emptyConsumer();
             });
 
-    // TODO: Require that the results of the two compilations are the same.
-    assertEquals(
-        app1.getDexProgramResources().size(),
-        app2.getDexProgramResources().size());
+    assertIdenticalApplicationsUpToCode(app1, app2, false);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
index 99cba4d..4ac8f0e 100644
--- a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
@@ -128,6 +128,7 @@
             null,
             options,
             null);
+        options.closeProgramConsumer();
         compatSink.close();
         return compatSink.build();
       } catch (ExecutionException | IOException e) {
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index cb50865..4a2e861 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -31,14 +31,14 @@
   protected ProcessResult runOnJavaRaw(JasminBuilder builder, String main) throws Exception {
     Path out = temp.newFolder().toPath();
     builder.writeClassFiles(out);
-    return ToolHelper.runJava(ImmutableList.of(out.toString()), main);
+    return ToolHelper.runJava(out, main);
   }
 
   protected ProcessResult runOnJavaNoVerifyRaw(JasminBuilder builder, String main)
       throws Exception {
     Path out = temp.newFolder().toPath();
     builder.writeClassFiles(out);
-    return ToolHelper.runJavaNoVerify(ImmutableList.of(out.toString()), main);
+    return ToolHelper.runJavaNoVerify(out, main);
   }
 
   protected ProcessResult runOnJavaNoVerifyRaw(JasminBuilder program, JasminBuilder library,
@@ -48,8 +48,7 @@
     program.writeClassFiles(out);
     Path libraryOut = temp.newFolder().toPath();
     library.writeClassFiles(libraryOut);
-    return ToolHelper.runJavaNoVerify(ImmutableList.of(out.toString(), libraryOut.toString()),
-        main);
+    return ToolHelper.runJavaNoVerify(ImmutableList.of(out, libraryOut), main);
   }
 
   private String assertNormalExitAndGetStdout(ProcessResult result) {
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 feecda0..9d9702f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -631,6 +631,7 @@
       executor.shutdown();
     }
     compatSink.close();
+    options.closeProgramConsumer();
     return compatSink.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
index 99577de..6c769aa 100644
--- a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
@@ -31,6 +31,22 @@
   }
 
   @Test
+  public void parseIdentifierArrowAmbiguity1() throws IOException {
+    ClassNameMapper mapper = ClassNameMapper.mapperFromString("a->b:");
+    ClassNameMapper.Builder builder = ClassNameMapper.builder();
+    builder.classNamingBuilder("b", "a");
+    Assert.assertEquals(builder.build(), mapper);
+  }
+
+  @Test
+  public void parseIdentifierArrowAmbiguity2() throws IOException {
+    ClassNameMapper mapper = ClassNameMapper.mapperFromString("-->b:");
+    ClassNameMapper.Builder builder = ClassNameMapper.builder();
+    builder.classNamingBuilder("b", "-");
+    Assert.assertEquals(builder.build(), mapper);
+  }
+
+  @Test
   public void parseMapWithPackageInfo() throws IOException {
     ClassNameMapper mapper = ClassNameMapper.mapperFromString(EXAMPLE_MAP_WITH_PACKAGE_INFO);
     Assert.assertTrue(mapper.getObfuscatedToOriginalMapping().isEmpty());
diff --git a/src/test/java/com/android/tools/r8/rewrite/longcompare/RequireNonNullRewriteTest.java b/src/test/java/com/android/tools/r8/rewrite/longcompare/RequireNonNullRewriteTest.java
index 39eebad..a1bece1 100644
--- a/src/test/java/com/android/tools/r8/rewrite/longcompare/RequireNonNullRewriteTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/longcompare/RequireNonNullRewriteTest.java
@@ -4,19 +4,18 @@
 package com.android.tools.r8.rewrite.longcompare;
 
 import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.D8;
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.InstructionSubject;
 import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
-import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -50,8 +49,7 @@
     builder.setMainClass(mainClass);
     try {
       String output = ToolHelper.runArt(builder);
-      ProcessResult javaResult = ToolHelper
-          .runJava(ImmutableList.of(jarFile.toString()), mainClass);
+      ProcessResult javaResult = ToolHelper.runJava(jarFile, mainClass);
       Assert.assertEquals(javaResult.stdout, output);
     } catch (IOException e) {
       Assert.fail();