Merge "Only fail with discard checks when tree-shaking"
diff --git a/.gitignore b/.gitignore
index 2ece82e3..86f3139 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,36 +22,24 @@
 third_party/android_jar/lib.tar.gz
 third_party/android_jar/lib-v[0-9][0-9]
 third_party/android_jar/lib-v[0-9][0-9].tar.gz
-third_party/gmscore/v4
-third_party/gmscore/v4.tar.gz
-third_party/gmscore/v5
-third_party/gmscore/v5.tar.gz
-third_party/gmscore/v6
-third_party/gmscore/v6.tar.gz
-third_party/gmscore/v7
-third_party/gmscore/v7.tar.gz
-third_party/gmscore/v8
-third_party/gmscore/v8.tar.gz
-third_party/gmscore/gmscore_v9
-third_party/gmscore/gmscore_v9.tar.gz
-third_party/gmscore/gmscore_v10
-third_party/gmscore/gmscore_v10.tar.gz
+third_party/gmail/*
+!third_party/gmail/*.sha1
+third_party/gmscore/*
+!third_party/gmscore/*.sha1
 third_party/gradle/gradle.tar.gz
 third_party/gradle/gradle
 third_party/jasmin.tar.gz
 third_party/jasmin
 third_party/jdwp-tests.tar.gz
 third_party/jdwp-tests
-third_party/proguard/proguard5.2.1.tar.gz
-third_party/proguard/proguard5.2.1
+third_party/photos/*
+!third_party/photos/*.sha1
+third_party/proguard/*
+!third_party/proguard/*.sha1
 third_party/proguardsettings.tar.gz
 third_party/proguardsettings/
-third_party/youtube/youtube.android_11.47
-third_party/youtube/youtube.android_11.47.tar.gz
-third_party/youtube/youtube.android_12.10
-third_party/youtube/youtube.android_12.10.tar.gz
-third_party/youtube/youtube.android_12.17/
-third_party/youtube/youtube.android_12.17.tar.gz
+third_party/youtube/*
+!third_party/youtube/*sha1
 third_party/jctf
 third_party/jctf.tar.gz
 third_party/android_cts_baseline
@@ -63,3 +51,4 @@
 #*#
 *~
 .#*
+
diff --git a/README.md b/README.md
index 9d93bc2..1bd795c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,83 @@
-The R8 repo contains two tools.
+# D8 dexer and R8 shrinker
 
-1. D8 is a dexer that converts java byte code to dex code.
+The R8 repo contains two tools:
 
-2. R8 is a java program shrinking and minification tool.
+- D8 is a dexer that converts java byte code to dex code.
+- R8 is a java program shrinking and minification tool that converts java byte
+  code to optimized dex code.
+
+D8 is a replacement for the DX dexer and R8 is a replacement for
+the [Proguard](https://www.guardsquare.com/en/proguard) shrinking and
+minification tool.
+
+## Downloading and building
+
+The R8 project uses [`depot_tools`](https://www.chromium.org/developers/how-tos/install-depot-tools)
+from the chromium project to manage dependencies. Install `depot_tools` and add it to
+your path before proceeding.
+
+The R8 project uses Java 8 language features and requires a Java 8 compiler
+and runtime system.
+
+Typical steps to download and build:
+
+
+    $ git clone https://r8.googlesource.com/r8
+    $ cd r8
+    $ tools/gradle.py d8 r8
+
+The `tools/gradle.py` script will bootstrap using depot_tools to download
+a version of gradle to use for building on the first run. This will produce
+two jar files: `build/libs/d8.jar` and `build/libs/r8.jar`.
+
+## Running D8
+
+The D8 dexer has a simple command-line interface with only a few options.
+
+The most important option is whether to build in debug or release mode.  Debug
+is the default mode and includes debugging information in the resulting dex
+files. Debugging information contains information about local variables used
+when debugging dex code. This information is not useful when shipping final
+Android apps to users and therefore, final builds should use the `--release`
+flag to remove this debugging information to produce smaller dex files.
+
+Typical invocations of D8 to produce dex file(s) in the out directoy:
+
+Debug mode build:
+
+    $ java -jar build/libs/d8.jar --output out input.jar
+
+Release mode build:
+
+    $ java -jar build/libs/d8.jar --release --output out input.jar
+
+The full set of D8 options can be obtained by running the command line tool with
+the `--help` option.
+
+## Running R8
+
+R8 is a [Proguard](https://www.guardsquare.com/en/proguard) replacement for
+whole-program optimization, shrinking and minification. R8 uses the Proguard
+keep rule format for specifying the entry points for an application.
+
+Typical invocations of R8 to produce optimized dex file(s) in the out directory:
+
+    $ java -jar build/libs/r8.jar --release --output out --pg-conf proguard.cfg input.jar
+
+The full set of R8 options can be obtained by running the command line tool with
+the `--help` option.
+
+## Testing
+
+Typical steps to run tests:
+
+    $ tools/test.py --no_internal
+
+The `tools/test.py` script will use depot_tools to download a lot of tests
+and test dependencies on the first run. This includes prebuilt version of the
+art runtime on which to validate the produced dex code.
+
+## Getting help
+
+For questions, bug reports and other issues reach out to us at
+r8-dev@googlegroups.com.
diff --git a/build.gradle b/build.gradle
index eb09333..d8402bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,8 @@
 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-import org.gradle.internal.os.OperatingSystem;
+import org.gradle.internal.os.OperatingSystem
+import utils.Utils;
 
 apply plugin: 'java'
 apply plugin: 'idea'
@@ -98,7 +99,8 @@
     examplesAndroidOCompile group: 'org.ow2.asm', name: 'asm', version: '5.1'
 }
 
-def osString = OperatingSystem.current().isLinux() ? "linux" : "mac"
+def osString = OperatingSystem.current().isLinux() ? "linux" :
+        OperatingSystem.current().isMacOsX() ? "mac" : "windows"
 
 def cloudDependencies = [
         "tests"      : [
@@ -107,6 +109,7 @@
         "third_party": [
                 "android_jar/lib-v14.tar.gz",
                 "android_jar/lib-v19.tar.gz",
+                "android_jar/lib-v21.tar.gz",
                 "android_jar/lib-v24.tar.gz",
                 "android_jar/lib-v25.tar.gz",
                 "android_jar/lib-v26.tar.gz",
@@ -136,14 +139,21 @@
             def sha1File = "${gzFile}.sha1"
             inputs.file sha1File
             outputs.file gzFile
-            executable "bash"
-            args "-c", "download_from_google_storage -n -b r8-deps -u -s ${sha1File}"
+            List<String> dlFromStorageArgs = ["-n", "-b", "r8-deps", "-u", "-s", "${sha1File}"]
+            if (OperatingSystem.current().isWindows()) {
+                executable "download_from_google_storage.bat"
+                args dlFromStorageArgs
+            } else {
+                executable "bash"
+                args "-c", "download_from_google_storage " + String.join(" ", dlFromStorageArgs)
+            }
         }
     }
 }
 
 def x20Dependencies = [
     "third_party": [
+        "gmail/gmail_android_170604.16.tar.gz",
         "gmscore/v4.tar.gz",
         "gmscore/v5.tar.gz",
         "gmscore/v6.tar.gz",
@@ -151,11 +161,13 @@
         "gmscore/v8.tar.gz",
         "gmscore/gmscore_v9.tar.gz",
         "gmscore/gmscore_v10.tar.gz",
+        "gmscore/latest.tar.gz",
         "photos/2017-06-06.tar.gz",
-        "youtube/youtube.android_11.47.tar.gz",
         "youtube/youtube.android_12.10.tar.gz",
         "youtube/youtube.android_12.17.tar.gz",
+        "youtube/youtube.android_12.22.tar.gz",
         "proguardsettings.tar.gz",
+        "proguard/proguard_internal_159423826.tar.gz",
     ],
 ]
 
@@ -370,7 +382,11 @@
     }
     task "dex_debuginfo_examples"(type: Exec,
             dependsOn: ["jar_debuginfo_examples", "downloadDeps"]) {
-        executable file("tools/linux/dx/bin/dx");
+        if (OperatingSystem.current().isWindows()) {
+            executable file("tools/windows/dx/bin/dx.bat")
+        } else {
+            executable file("tools/linux/dx/bin/dx");
+        }
         args "--dex"
         args "--output=build/test/${hostDexJar}"
         args "build/test/${hostJar}"
@@ -421,7 +437,12 @@
 task buildExampleJars {
     dependsOn downloadProguard
     def examplesDir = file("src/test/examples")
-    def proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.sh"
+    def proguardScript
+    if (OperatingSystem.current().isWindows()) {
+        proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.bat"
+    } else {
+        proguardScript = "third_party/proguard/proguard5.2.1/bin/proguard.sh"
+    }
     task "compile_examples"(type: JavaCompile) {
         source = fileTree(dir: examplesDir, include: '**/*.java')
         destinationDir = file("build/test/examples/classes")
@@ -453,11 +474,17 @@
                 // Enable these to get stdout and stderr redirected to files...
                 // standardOutput = new FileOutputStream('proguard.stdout')
                 // errorOutput = new FileOutputStream('proguard.stderr')
-                executable "bash"
-                args "-c", "${proguardScript} '-verbose -dontwarn java.** -injars ${jarPath}" +
+                def proguardArguments = "-verbose -dontwarn java.** -injars ${jarPath}" +
                         " -outjars ${proguardJarPath}" +
                         " -include ${proguardConfigPath}" +
-                        " -printmapping ${proguardMapPath}'"
+                        " -printmapping ${proguardMapPath}"
+                if (OperatingSystem.current().isWindows()) {
+                    executable "${proguardScript}"
+                    args "${proguardArguments}"
+                } else {
+                    executable "bash"
+                    args "-c", "${proguardScript} '${proguardArguments}'"
+                }
                 outputs.file proguardJarPath
             }
         } else {
@@ -560,11 +587,12 @@
 }
 
 task buildExamples {
-    if (OperatingSystem.current().isMacOsX()) {
-        logger.lifecycle("WARNING: Testing (including building examples) is only partially supported on Mac OS.")
+    if (OperatingSystem.current().isMacOsX() || OperatingSystem.current().isWindows()) {
+        logger.lifecycle("WARNING: Testing (including building examples) is only partially supported on your " +
+                "platform (" + OperatingSystem.current().getName() + ").")
     } else if (!OperatingSystem.current().isLinux()) {
       logger.lifecycle("WARNING: Testing (including building examples) is not supported on your platform. " +
-          "It is fully supported on Linux and partially supported on Mac OS")
+          "It is fully supported on Linux and partially supported on Mac OS and Windows")
       return;
     }
     dependsOn buildDebugTestResourcesJars
@@ -659,6 +687,9 @@
     forkEvery = 0
     // Use the Concurrent Mark Sweep GC (CMS) to keep memory usage at a resonable level.
     jvmArgs = ["-XX:+UseConcMarkSweepGC"]
+    if (project.hasProperty('disable_assertions')) {
+        enableAssertions = false
+    }
 }
 
 task buildPreNJdwpTestsJar(type: Jar) {
@@ -725,12 +756,18 @@
         systemProperty 'jctf_compile_only', '1'
     }
 
-    if (OperatingSystem.current().isLinux() || OperatingSystem.current().isMacOsX()) {
+    if (OperatingSystem.current().isLinux()
+            || OperatingSystem.current().isMacOsX()
+            || OperatingSystem.current().isWindows()) {
         if (OperatingSystem.current().isMacOsX()) {
             logger.lifecycle("WARNING: Testing in only partially supported on Mac OS. " +
                 "Art only runs on Linux and tests requiring Art runs in a Docker container, which must be present. " +
                 "See tools/docker/README.md for details.")
         }
+        if (OperatingSystem.current().isWindows()) {
+            logger.lifecycle("WARNING: Testing in only partially supported on Windows. " +
+                    "Art only runs on Linux and tests requiring Art will be skipped")
+        }
         dependsOn downloadDeps
         dependsOn buildExamples
         dependsOn buildSmali
@@ -740,7 +777,7 @@
         dependsOn buildPreNJdwpTestsJar
     } else {
         logger.lifecycle("WARNING: Testing in not supported on your platform. Testing is only fully supported on " +
-            "Linux and partially supported on Mac OS. Art does not run on other platforms.")
+            "Linux and partially supported on Mac OS and Windows. Art does not run on other platforms.")
     }
 }
 
@@ -953,7 +990,7 @@
     def artTestDir = file("${androidCheckoutDir}/art/test")
     def artRunTestScript = file("${artTestDir}/run-test")
     def dxExecutable = new File("tools/linux/dx/bin/dx");
-    def dexMergerExecutable = new File("tools/linux/dx/bin/dexmerger");
+    def dexMergerExecutable = Utils.dexMergerExecutable()
     def dexToolName = dexTool == DexTool.DX ? "dx" : "jack"
 
     def name = dir.getName();
diff --git a/buildSrc/src/main/java/dx/DexMerger.java b/buildSrc/src/main/java/dx/DexMerger.java
index 39a4bfb..d3d8427 100644
--- a/buildSrc/src/main/java/dx/DexMerger.java
+++ b/buildSrc/src/main/java/dx/DexMerger.java
@@ -53,7 +53,7 @@
       public void execute(ExecSpec execSpec) {
         try {
           if (dexMergerExecutable == null) {
-            dexMergerExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/dexmerger");
+            dexMergerExecutable = Utils.dexMergerExecutable();
           }
           execSpec.setExecutable(dexMergerExecutable);
           execSpec.args(destination.getCanonicalPath());
diff --git a/buildSrc/src/main/java/dx/Dx.java b/buildSrc/src/main/java/dx/Dx.java
index e4b89c4..86d1bce 100644
--- a/buildSrc/src/main/java/dx/Dx.java
+++ b/buildSrc/src/main/java/dx/Dx.java
@@ -64,7 +64,8 @@
       public void execute(ExecSpec execSpec) {
         try {
           if (dxExecutable == null) {
-            dxExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/dx");
+            String dxExecutableName = Utils.toolsDir().equals("windows") ? "dx.bat" : "dx";
+            dxExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/" + dxExecutableName);
           }
           execSpec.setExecutable(dxExecutable);
           execSpec.args("--dex");
diff --git a/buildSrc/src/main/java/utils/Utils.java b/buildSrc/src/main/java/utils/Utils.java
index 5e5e9d7..7158e80 100644
--- a/buildSrc/src/main/java/utils/Utils.java
+++ b/buildSrc/src/main/java/utils/Utils.java
@@ -3,8 +3,22 @@
 // BSD-style license that can be found in the LICENSE file.
 package utils;
 
+import java.io.File;
+
 public class Utils {
   public static String toolsDir() {
-    return System.getProperty("os.name").equals("Mac OS X") ? "mac" : "linux";
+    String osName = System.getProperty("os.name");
+    if (osName.equals("Mac OS X")) {
+      return "mac";
+    } else if (osName.contains("Windows")) {
+      return "windows";
+    } else {
+      return "linux";
+    }
   }
-}
+
+  public static File dexMergerExecutable() {
+    String executableName = Utils.toolsDir().equals("windows") ? "dexmerger.bat" : "dexmerger";
+    return new File("tools/" + Utils.toolsDir() + "/dx/bin/" + executableName);
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 3497778..85c430a 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -5,13 +5,12 @@
 
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.List;
 
 abstract class BaseCommand {
 
@@ -147,7 +146,7 @@
     }
 
     /** Add library file resources. */
-    public B addLibraryFiles(List<Path> files) throws IOException {
+    public B addLibraryFiles(Collection<Path> files) throws IOException {
       app.addLibraryFiles(files);
       return self();
     }
diff --git a/src/main/java/com/android/tools/r8/BaseOutput.java b/src/main/java/com/android/tools/r8/BaseOutput.java
index cb4d473..a1fbb8e 100644
--- a/src/main/java/com/android/tools/r8/BaseOutput.java
+++ b/src/main/java/com/android/tools/r8/BaseOutput.java
@@ -49,6 +49,7 @@
    * Write the output resources to a zip-archive or directory.
    *
    * @param output Path to existing directory or non-existing zip-archive.
+   * @param overwrite true to allow overwriting existing files with outputs.
    */
-  abstract public void write(Path output) throws IOException;
+  abstract public void write(Path output, boolean overwrite) throws IOException;
 }
diff --git a/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
new file mode 100644
index 0000000..7dbb420
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -0,0 +1,27 @@
+// 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 java.util.Set;
+
+/**
+ * Represents a provider for application resources of class file kind.
+ *
+ * Note that the classes will only be created for resources provided by
+ * resource providers on-demand when they are needed by the tool. If
+ * never needed, the resource will never be loaded.
+ */
+public interface ClassFileResourceProvider {
+  /** Returns all class descriptors. */
+  Set<String> getClassDescriptors();
+
+  /**
+   * Get the class resource associated with the descriptor, or null if
+   * this provider does not have one.
+   *
+   * Method may be called several times for the same resource, and should
+   * support concurrent calls from different threads.
+   */
+  Resource getResource(String descriptor);
+}
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 0a0c976..522f6f0 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.ApplicationWriter;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -59,11 +60,12 @@
    * @return the compilation result.
    */
   public static D8Output run(D8Command command) throws IOException {
-    CompilationResult result = runForTesting(command.getInputApp(), command.getInternalOptions());
+    InternalOptions options = command.getInternalOptions();
+    CompilationResult result = runForTesting(command.getInputApp(), options);
     assert result != null;
     D8Output output = new D8Output(result.androidApp, command.getOutputMode());
     if (command.getOutputPath() != null) {
-      output.write(command.getOutputPath());
+      output.write(command.getOutputPath(), options.overwriteOutputs);
     }
     return output;
   }
@@ -79,12 +81,13 @@
    * @return the compilation result.
    */
   public static D8Output run(D8Command command, ExecutorService executor) throws IOException {
+    InternalOptions options = command.getInternalOptions();
     CompilationResult result = runForTesting(
-        command.getInputApp(), command.getInternalOptions(), executor);
+        command.getInputApp(), options, executor);
     assert result != null;
     D8Output output = new D8Output(result.androidApp, command.getOutputMode());
     if (command.getOutputPath() != null) {
-      output.write(command.getOutputPath());
+      output.write(command.getOutputPath(), options.overwriteOutputs);
     }
     return output;
   }
@@ -162,7 +165,7 @@
       app = optimize(app, appInfo, options);
 
       // If a method filter is present don't produce output since the application is likely partial.
-      if (!options.methodsFilter.isEmpty()) {
+      if (options.hasMethodsFilter()) {
         System.out.println("Finished compilation with method filter: ");
         options.methodsFilter.forEach((m) -> System.out.println("  - " + m));
         return null;
@@ -178,13 +181,17 @@
       options.printWarnings();
       return output;
     } catch (ExecutionException e) {
-      throw new RuntimeException(e.getMessage(), e.getCause());
+      if (e.getCause() instanceof CompilationError) {
+        throw (CompilationError) e.getCause();
+      } else {
+        throw new RuntimeException(e.getMessage(), e.getCause());
+      }
     }
   }
 
   private static DexApplication optimize(
       DexApplication application, AppInfo appInfo, InternalOptions options)
-      throws IOException, ExecutionException {
+      throws IOException {
     final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
     IRConverter converter = new IRConverter(application, appInfo, options, printer);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 0ad793c..62cb5a5 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -3,15 +3,20 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.android.tools.r8.utils.OutputMode;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
 
 /**
  * Immutable command structure for an invocation of the {@code D8} compiler.
@@ -39,12 +44,46 @@
       super(app, CompilationMode.DEBUG);
     }
 
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    /** Add classpath file resources. */
+    public Builder addClasspathFiles(Path... files) throws IOException {
+      return addClasspathFiles(Arrays.asList(files));
+    }
+
+    /** Add classpath file resources. */
+    public Builder addClasspathFiles(Collection<Path> files) throws IOException {
+      for (Path file : files) {
+        if (isArchive(file)) {
+          addClasspathResourceProvider(PreloadedClassFileProvider.fromArchive(file));
+        } else {
+          super.addClasspathFiles(file);
+        }
+      }
+      return this;
+    }
+
+    /** Add library file resources. */
+    public Builder addLibraryFiles(Path... files) throws IOException {
+      return addLibraryFiles(Arrays.asList(files));
+    }
+
+    /** Add library file resources. */
+    public Builder addLibraryFiles(Collection<Path> files) throws IOException {
+      for (Path file : files) {
+        if (isArchive(file)) {
+          addLibraryResourceProvider(PreloadedClassFileProvider.fromArchive(file));
+        } else {
+          super.addLibraryFiles(file);
+        }
+      }
+      return this;
+    }
+
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addClasspathResourceProvider(provider);
       return this;
     }
 
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addLibraryResourceProvider(provider);
       return this;
     }
@@ -155,7 +194,7 @@
     assert !internal.debug;
     internal.debug = getMode() == CompilationMode.DEBUG;
     internal.minApiLevel = getMinApiLevel();
-    internal.fillDexFiles = true;
+    internal.overwriteOutputs = true;
     // Assert and fixup defaults.
     assert !internal.skipMinification;
     internal.skipMinification = true;
@@ -171,8 +210,6 @@
     internal.removeSwitchMaps = false;
     assert internal.outline.enabled;
     internal.outline.enabled = false;
-    internal.lazyClasspathLoading = true;
-    internal.lazyLibraryLoading = true;
     internal.outputMode = getOutputMode();
     return internal;
   }
diff --git a/src/main/java/com/android/tools/r8/D8Output.java b/src/main/java/com/android/tools/r8/D8Output.java
index 1910e9b..4ba7067 100644
--- a/src/main/java/com/android/tools/r8/D8Output.java
+++ b/src/main/java/com/android/tools/r8/D8Output.java
@@ -16,7 +16,7 @@
   }
 
   @Override
-  public void write(Path output) throws IOException {
-    getAndroidApp().write(output, getOutputMode());
+  public void write(Path output, boolean overwrite) throws IOException {
+    getAndroidApp().write(output, getOutputMode(), overwrite);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 71daa5c..b183ecc 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -4,19 +4,145 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.stream.Collectors;
 
 public class Disassemble {
+  public static class DisassembleCommand extends BaseCommand {
+
+    public static class Builder
+        extends BaseCommand.Builder<DisassembleCommand, DisassembleCommand.Builder> {
+      private boolean useSmali = false;
+
+      private Builder() {
+        super(CompilationMode.RELEASE);
+      }
+
+      @Override
+      DisassembleCommand.Builder self() {
+        return this;
+      }
+
+      public DisassembleCommand.Builder setProguardMapFile(Path path) {
+        getAppBuilder().setProguardMapFile(path);
+        return this;
+      }
+
+      public DisassembleCommand.Builder setUseSmali(boolean useSmali) {
+        this.useSmali = useSmali;
+        return this;
+      }
+
+      @Override
+      public DisassembleCommand build() throws CompilationException, IOException {
+        // If printing versions ignore everything else.
+        if (isPrintHelp() || isPrintVersion()) {
+          return new DisassembleCommand(isPrintHelp(), isPrintVersion());
+        }
+
+        return new DisassembleCommand(
+            getAppBuilder().build(),
+            getOutputPath(),
+            getOutputMode(),
+            getMode(),
+            getMinApiLevel(),
+            useSmali);
+      }
+    }
+
+    static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
+        "Usage: disasm [options] <input-files>",
+        " where <input-files> are dex files",
+        " and options are:",
+        "  --smali                 # Disassemble using smali syntax.",
+        "  --pg-map <file>         # Proguard map <file> for mapping names.",
+        "  --version               # Print the version of r8.",
+        "  --help                  # Print this message."));
+
+
+    private final boolean useSmali;
+
+    public static DisassembleCommand.Builder builder() {
+      return new DisassembleCommand.Builder();
+    }
+
+    public static DisassembleCommand.Builder parse(String[] args)
+        throws CompilationException, IOException {
+      DisassembleCommand.Builder builder = builder();
+      parse(args, builder);
+      return builder;
+    }
+
+    private static void parse(String[] args, DisassembleCommand.Builder builder)
+        throws CompilationException, IOException {
+      for (int i = 0; i < args.length; i++) {
+        String arg = args[i].trim();
+        if (arg.length() == 0) {
+          continue;
+        } else if (arg.equals("--help")) {
+          builder.setPrintHelp(true);
+        } else if (arg.equals("--version")) {
+          builder.setPrintVersion(true);
+        } else if (arg.equals("--smali")) {
+          builder.setUseSmali(true);
+        } else if (arg.equals("--pg-map")) {
+          builder.setProguardMapFile(Paths.get(args[++i]));
+        } else {
+          if (arg.startsWith("--")) {
+            throw new CompilationException("Unknown option: " + arg);
+          }
+          builder.addProgramFiles(Paths.get(arg));
+        }
+      }
+    }
+
+    private DisassembleCommand(
+        AndroidApp inputApp,
+        Path outputPath,
+        OutputMode outputMode,
+        CompilationMode mode,
+        int minApiLevel,
+        boolean useSmali) {
+      super(inputApp, outputPath, outputMode, mode, minApiLevel);
+      //assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
+      this.useSmali = useSmali;
+    }
+
+    private DisassembleCommand(boolean printHelp, boolean printVersion) {
+      super(printHelp, printVersion);
+      this.useSmali = false;
+    }
+
+    public boolean useSmali() {
+      return useSmali;
+    }
+
+    @Override
+    InternalOptions getInternalOptions() {
+      InternalOptions internal = new InternalOptions();
+      internal.useSmaliSyntax = useSmali;
+      return internal;
+    }
+  }
+
   public static void main(String[] args)
       throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
-    List<Path> files = Arrays.stream(args).map(s -> Paths.get(s)).collect(Collectors.toList());
-    R8Command command = R8Command.builder().addProgramFiles(files).build();
+    DisassembleCommand.Builder builder = DisassembleCommand.parse(args);
+    DisassembleCommand command = builder.build();
+    if (command.isPrintHelp()) {
+      System.out.println(DisassembleCommand.USAGE_MESSAGE);
+      return;
+    }
+    if (command.isPrintVersion()) {
+      System.out.println("R8 v0.0.1");
+      return;
+    }
     R8.disassemble(command);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 390e752..11cc9bc 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -39,14 +39,12 @@
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.AttributeRemovalOptions;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
 import java.io.ByteArrayOutputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintStream;
@@ -55,6 +53,7 @@
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -145,7 +144,8 @@
     return result;
   }
 
-  public static void disassemble(R8Command command) throws IOException, ExecutionException {
+  public static void disassemble(Disassemble.DisassembleCommand command)
+      throws IOException, ExecutionException {
     Path output = command.getOutputPath();
     AndroidApp app = command.getInputApp();
     InternalOptions options = command.getInternalOptions();
@@ -172,7 +172,7 @@
   }
 
   static CompilationResult runForTesting(AndroidApp app, InternalOptions options)
-      throws ProguardRuleParserException, ExecutionException, IOException {
+      throws ProguardRuleParserException, IOException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
       return runForTesting(app, options, executor);
@@ -185,12 +185,12 @@
       AndroidApp app,
       InternalOptions options,
       ExecutorService executor)
-      throws ProguardRuleParserException, ExecutionException, IOException {
+      throws ProguardRuleParserException, IOException {
     return new R8(options).run(app, executor);
   }
 
   private CompilationResult run(AndroidApp inputApp, ExecutorService executorService)
-      throws IOException, ExecutionException, ProguardRuleParserException {
+      throws IOException, ProguardRuleParserException {
     if (options.quiet) {
       System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
     }
@@ -207,7 +207,6 @@
       RootSet rootSet;
       byte[] proguardSeedsData = null;
       timing.begin("Strip unused code");
-      Set<DexType> mainDexBaseClasses = null;
       try {
         Set<DexType> missingClasses = appInfo.getMissingClasses();
         missingClasses = filterMissingClasses(missingClasses, options.dontWarnPatterns);
@@ -281,7 +280,7 @@
         // Lets find classes which may have code executed before secondary dex files installation.
         RootSet mainDexRootSet =
             new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executorService);
-        mainDexBaseClasses = enqueuer.traceMainDex(mainDexRootSet, timing);
+        Set<DexType> mainDexBaseClasses = enqueuer.traceMainDex(mainDexRootSet, timing);
 
         // Calculate the automatic main dex list according to legacy multidex constraints.
         // Add those classes to an eventual manual list of classes.
@@ -324,7 +323,7 @@
       timing.end();
 
       // If a method filter is present don't produce output since the application is likely partial.
-      if (!options.methodsFilter.isEmpty()) {
+      if (options.hasMethodsFilter()) {
         System.out.println("Finished compilation with method filter: ");
         options.methodsFilter.forEach((m) -> System.out.println("  - " + m));
         return null;
@@ -348,21 +347,14 @@
               packageDistribution,
               options);
 
-      if (options.printMapping && androidApp.hasProguardMap() && !options.skipMinification) {
-        Path mapOutput = options.printMappingFile;
-        try (Closer closer = Closer.create()) {
-          OutputStream mapOut;
-          if (mapOutput == null) {
-            mapOut = System.out;
-          } else {
-            mapOut = new FileOutputStream(mapOutput.toFile());
-            closer.register(mapOut);
-          }
-          androidApp.writeProguardMap(closer, mapOut);
-        }
-      }
       options.printWarnings();
       return new CompilationResult(androidApp, application, appInfo);
+    } catch (ExecutionException e) {
+      if (e.getCause() instanceof CompilationError) {
+        throw (CompilationError) e.getCause();
+      } else {
+        throw new RuntimeException(e.getMessage(), e.getCause());
+      }
     } finally {
       // Dump timings.
       if (options.printTimes) {
@@ -381,13 +373,66 @@
    * @return the compilation result.
    */
   public static AndroidApp run(R8Command command)
-      throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
-    AndroidApp outputApp =
-        runForTesting(command.getInputApp(), command.getInternalOptions()).androidApp;
-    if (command.getOutputPath() != null) {
-      outputApp.write(command.getOutputPath(), OutputMode.Indexed);
+      throws IOException, CompilationException, ProguardRuleParserException {
+    ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+    try {
+      return run(command, executorService);
+    } finally {
+      executorService.shutdown();
     }
-    return outputApp;
+  }
+
+  static void writeOutputs(R8Command command, InternalOptions options, AndroidApp outputApp)
+      throws IOException {
+    if (command.getOutputPath() != null) {
+      outputApp.write(command.getOutputPath(), options.outputMode, options.overwriteOutputs);
+    }
+
+    if (options.printMapping && !options.skipMinification) {
+      assert outputApp.hasProguardMap();
+      try (Closer closer = Closer.create()) {
+        OutputStream mapOut = openPathWithDefault(
+            closer,
+            options.printMappingFile,
+            options.overwriteOutputs,
+            System.out);
+        outputApp.writeProguardMap(closer, mapOut);
+      }
+    }
+    if (options.printSeeds) {
+      assert outputApp.hasProguardSeeds();
+      try (Closer closer = Closer.create()) {
+        OutputStream seedsOut =
+            openPathWithDefault(closer, options.seedsFile, options.overwriteOutputs, System.out);
+        outputApp.writeProguardSeeds(closer, seedsOut);
+      }
+    }
+    if (options.printMainDexList && outputApp.hasMainDexList()) {
+      try (Closer closer = Closer.create()) {
+        OutputStream mainDexOut =
+            openPathWithDefault(closer, options.printMainDexListFile, true, System.out);
+        outputApp.writeMainDexList(closer, mainDexOut);
+      }
+    }
+  }
+
+  private static OutputStream openPathWithDefault(Closer closer,
+      Path file,
+      boolean allowOverwrite,
+      PrintStream defaultOutput) throws IOException {
+    OutputStream mapOut;
+    if (file == null) {
+      mapOut = defaultOutput;
+    } else {
+      if (!allowOverwrite) {
+        mapOut = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW);
+      } else {
+        mapOut = Files.newOutputStream(file,
+            StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+      }
+      closer.register(mapOut);
+    }
+    return mapOut;
   }
 
   /**
@@ -401,17 +446,16 @@
    * @return the compilation result.
    */
   public static AndroidApp run(R8Command command, ExecutorService executor)
-      throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
+      throws IOException, CompilationException, ProguardRuleParserException {
+    InternalOptions options = command.getInternalOptions();
     AndroidApp outputApp =
-        runForTesting(command.getInputApp(), command.getInternalOptions(), executor).androidApp;
-    if (command.getOutputPath() != null) {
-      outputApp.write(command.getOutputPath(), OutputMode.Indexed);
-    }
+        runForTesting(command.getInputApp(), options, executor).androidApp;
+    writeOutputs(command, options, outputApp);
     return outputApp;
   }
 
   private static void run(String[] args)
-      throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
+      throws IOException, ProguardRuleParserException, CompilationException {
     R8Command.Builder builder = R8Command.parse(args);
     if (builder.getOutputPath() == null) {
       builder.setOutputPath(Paths.get("."));
@@ -442,7 +486,7 @@
     } catch (ProguardRuleParserException e) {
       System.err.println("Failed parsing proguard keep rules: " + e.getMessage());
       System.exit(1);
-    } catch (RuntimeException | ExecutionException e) {
+    } catch (RuntimeException e) {
       System.err.println("Compilation failed with an internal error.");
       Throwable cause = e.getCause() == null ? e : e.getCause();
       cause.printStackTrace();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9b44546..06ebe60 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -26,6 +26,7 @@
   public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
 
     private final List<Path> mainDexRules = new ArrayList<>();
+    private boolean minimalMainDex = false;
     private final List<Path> proguardConfigFiles = new ArrayList<>();
     private Optional<Boolean> treeShaking = Optional.empty();
     private Optional<Boolean> minification = Optional.empty();
@@ -77,6 +78,16 @@
     }
 
     /**
+     * Request minimal main dex generated when main dex rules are used.
+     *
+     * The main purpose of this is to verify that the main dex rules are sufficient
+     * for running on a platform without native multi dex support.
+     */
+    public Builder setMinimalMainDex(boolean value) {
+      minimalMainDex = value;
+      return this;
+    }
+    /**
      * Add proguard configuration file resources.
      */
     public Builder addProguardConfigurationFiles(Path... paths) {
@@ -161,6 +172,7 @@
           getOutputPath(),
           getOutputMode(),
           mainDexKeepRules,
+          minimalMainDex,
           configuration,
           getMode(),
           getMinApiLevel(),
@@ -196,6 +208,7 @@
       "  --help                  # Print this message."));
 
   private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+  private final boolean minimalMainDex;
   private final ProguardConfiguration proguardConfiguration;
   private final boolean useTreeShaking;
   private final boolean useMinification;
@@ -259,6 +272,8 @@
         builder.setMinification(false);
       } else if (arg.equals("--multidex-rules")) {
         builder.addMainDexRules(Paths.get(args[++i]));
+      } else if (arg.equals("--minimal-maindex")) {
+        builder.setMinimalMainDex(true);
       } else if (arg.equals("--pg-conf")) {
         builder.addProguardConfigurationFiles(Paths.get(args[++i]));
       } else if (arg.equals("--pg-map")) {
@@ -300,6 +315,7 @@
       Path outputPath,
       OutputMode outputMode,
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+      boolean minimalMainDex,
       ProguardConfiguration proguardConfiguration,
       CompilationMode mode,
       int minApiLevel,
@@ -311,6 +327,7 @@
     assert mainDexKeepRules != null;
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
     this.mainDexKeepRules = mainDexKeepRules;
+    this.minimalMainDex = minimalMainDex;
     this.proguardConfiguration = proguardConfiguration;
     this.useTreeShaking = useTreeShaking;
     this.useMinification = useMinification;
@@ -320,6 +337,7 @@
   private R8Command(boolean printHelp, boolean printVersion) {
     super(printHelp, printVersion);
     mainDexKeepRules = ImmutableList.of();
+    minimalMainDex = false;
     proguardConfiguration = null;
     useTreeShaking = false;
     useMinification = false;
@@ -346,6 +364,7 @@
     internal.useTreeShaking = useTreeShaking();
     assert !internal.ignoreMissingClasses;
     internal.ignoreMissingClasses = ignoreMissingClasses;
+    internal.overwriteOutputs = true;
 
     // TODO(zerny): Consider which other proguard options should be given flags.
     assert internal.packagePrefix.length() == 0;
@@ -375,6 +394,7 @@
     internal.classObfuscationDictionary = proguardConfiguration.getClassObfuscationDictionary();
     internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary();
     internal.mainDexKeepRules = mainDexKeepRules;
+    internal.minimalMainDex = minimalMainDex;
     internal.keepRules = proguardConfiguration.getRules();
     internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns();
     internal.outputMode = getOutputMode();
diff --git a/src/main/java/com/android/tools/r8/ReadMainDexList.java b/src/main/java/com/android/tools/r8/ReadMainDexList.java
new file mode 100644
index 0000000..3a76c1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ReadMainDexList.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ProguardMapReader;
+import com.android.tools.r8.utils.FileUtils;
+import java.nio.file.Paths;
+import java.util.stream.Collectors;
+
+/**
+ * Utility for applying proguard map and sorting the main dex list.
+ */
+public class ReadMainDexList {
+
+  private String DOT_CLASS = ".class";
+
+  private String stripDotClass(String name) {
+    return name.endsWith(DOT_CLASS) ? name.substring(0, name.length() - DOT_CLASS.length()) : name;
+  }
+
+  private String addDotClass(String name) {
+    return name + DOT_CLASS;
+  }
+
+  private String deobfuscateClassName(String name, ClassNameMapper mapper) {
+    if (mapper == null) {
+      return name;
+    }
+    return mapper.deobfuscateClassName(name);
+  }
+
+  private void run(String[] args) throws Exception {
+    if (args.length != 1 && args.length != 2) {
+      System.out.println("Usage: command <main_dex_list> [<proguard_map>]");
+      System.exit(0);
+    }
+
+    final ClassNameMapper mapper =
+        args.length == 2 ? ProguardMapReader.mapperFromFile(Paths.get(args[1])) : null;
+
+    FileUtils.readTextFile(Paths.get(args[0]))
+        .stream()
+        .map(this::stripDotClass)
+        .map(name -> name.replace('/', '.'))
+        .map(name -> deobfuscateClassName(name, mapper))
+        .map(name -> name.replace('.', '/'))
+        .map(this::addDotClass)
+        .sorted()
+        .collect(Collectors.toList())
+        .forEach(System.out::println);
+  }
+
+  public static void main(String[] args) throws Exception {
+    new ReadMainDexList().run(args);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ResourceProvider.java b/src/main/java/com/android/tools/r8/ResourceProvider.java
deleted file mode 100644
index 2a5b0dc..0000000
--- a/src/main/java/com/android/tools/r8/ResourceProvider.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8;
-
-/**
- * Represents a provider for application resources. All resources returned
- * via this provider should be class file resources and will be loaded on-demand
- * or not loaded at all.
- *
- * NOTE: currently only resources representing Java class files can be loaded
- * with lazy resource providers.
- */
-public interface ResourceProvider {
-  /** Get the class resource associated with the descriptor, or null. */
-  Resource getResource(String descriptor);
-}
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index 945578a..148c167 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -8,8 +8,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexApplication.Builder;
-import com.android.tools.r8.graph.DexClassPromise;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.NamingLens;
@@ -24,7 +22,6 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -261,23 +258,20 @@
     System.out.println("Next bisection range: " + nextRange);
     int goodClasses = 0;
     int badClasses = 0;
-    Map<DexType, DexClassPromise> classes = new HashMap<>();
-    for (DexLibraryClass clazz : badApp.libraryClasses()) {
-      classes.put(clazz.type, clazz);
-    }
+    List<DexProgramClass> programClasses = new ArrayList<>();
     for (DexProgramClass clazz : badApp.classes()) {
       DexProgramClass goodClass = getGoodClass(clazz);
       if (goodClass != null) {
-        classes.put(goodClass.type, goodClass);
+        programClasses.add(goodClass);
         ++goodClasses;
       } else {
-        classes.put(clazz.type, clazz);
+        programClasses.add(clazz);
         assert !nextRange.isEmpty();
         ++badClasses;
       }
     }
     System.out.println("Class split is good: " + goodClasses + ", bad: " + badClasses);
-    return new Builder(badApp, classes).build();
+    return new Builder(badApp).replaceProgramClasses(programClasses).build();
   }
 
   private DexProgramClass getGoodClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index c174a31..4a499ec 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -484,7 +484,7 @@
       }
       writeZipWithClasses(inputs, result, output);
     } else {
-      result.write(output);
+      result.write(output, true);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index af6bebe..c7a66c2 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -8,22 +8,29 @@
 import static com.android.tools.r8.dex.Constants.ANDROID_O_API;
 import static com.android.tools.r8.dex.Constants.ANDROID_O_DEX_VERSION;
 import static com.android.tools.r8.dex.Constants.DEFAULT_ANDROID_API;
+import static com.android.tools.r8.graph.ClassKind.CLASSPATH;
+import static com.android.tools.r8.graph.ClassKind.LIBRARY;
+import static com.android.tools.r8.graph.ClassKind.PROGRAM;
 import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
-import com.android.tools.r8.graph.LazyClassFileLoader;
 import com.android.tools.r8.naming.ProguardMapReader;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ClassProvider;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexList;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -32,7 +39,8 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -67,107 +75,25 @@
     final DexApplication.Builder builder = new DexApplication.Builder(itemFactory, timing);
     try (Closer closer = Closer.create()) {
       List<Future<?>> futures = new ArrayList<>();
+      // Still preload some of the classes, primarily for two reasons:
+      // (a) class lazy loading is not supported for DEX files
+      //     now and current implementation of parallel DEX file
+      //     loading will be lost with on-demand class loading.
+      // (b) some of the class file resources don't provide information
+      //     about class descriptor.
+      // TODO: try and preload less classes.
       readProguardMap(builder, executorService, futures, closer);
       readMainDexList(builder, executorService, futures, closer);
-      readDexSources(builder, executorService, futures, closer);
-      readClassSources(builder, closer);
-      initializeLazyClassCollection(builder);
+      ClassReader classReader = new ClassReader(executorService, futures, closer);
+      classReader.readSources();
       ThreadUtils.awaitFutures(futures);
-    } catch (ExecutionException e) {
-      // If the reading failed with a valid compilation error, rethrow the unwrapped exception.
-      Throwable cause = e.getCause();
-      if (cause != null && cause instanceof CompilationError) {
-        throw (CompilationError) cause;
-      }
-      throw e;
+      classReader.initializeLazyClassCollection(builder);
     } finally {
       timing.end();
     }
     return builder.build();
   }
 
-  private void readClassSources(DexApplication.Builder builder, Closer closer)
-      throws IOException, ExecutionException {
-    JarApplicationReader application = new JarApplicationReader(options);
-    JarClassFileReader reader = new JarClassFileReader(
-        application, builder::addClassIgnoringLibraryDuplicates);
-    for (Resource input : inputApp.getClassProgramResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.PROGRAM, input.getStream(closer));
-    }
-    for (Resource input : inputApp.getClassClasspathResources()) {
-      if (options.lazyClasspathLoading && getResourceClassDescriptorOrNull(input) != null) {
-        addLazyLoader(application, ClassKind.CLASSPATH, builder, input);
-      } else {
-        reader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, input.getStream(closer));
-      }
-    }
-    for (Resource input : inputApp.getClassLibraryResources()) {
-      if (options.lazyLibraryLoading && getResourceClassDescriptorOrNull(input) != null) {
-        addLazyLoader(application, ClassKind.LIBRARY, builder, input);
-      } else {
-        reader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, input.getStream(closer));
-      }
-    }
-  }
-
-  private void initializeLazyClassCollection(DexApplication.Builder builder) {
-    List<ResourceProvider> classpathProviders = inputApp.getClasspathResourceProviders();
-    List<ResourceProvider> libraryProviders = inputApp.getLibraryResourceProviders();
-    if (!classpathProviders.isEmpty() || !libraryProviders.isEmpty()) {
-      builder.setLazyClassCollection(new LazyClassCollection(
-          new JarApplicationReader(options), classpathProviders, libraryProviders));
-    }
-  }
-
-  private void addLazyLoader(JarApplicationReader application,
-      ClassKind classKind, DexApplication.Builder builder, Resource resource) {
-    // Generate expected DEX type.
-    String classDescriptor = getResourceClassDescriptorOrNull(resource);
-    assert classDescriptor != null;
-    DexType type = options.itemFactory.createType(classDescriptor);
-    LazyClassFileLoader newLoader = new LazyClassFileLoader(type, resource, classKind, application);
-    builder.addClassPromise(newLoader, true);
-  }
-
-  private void readDexSources(DexApplication.Builder builder, ExecutorService executorService,
-      List<Future<?>> futures, Closer closer)
-      throws IOException, ExecutionException {
-    List<Resource> dexProgramSources = inputApp.getDexProgramResources();
-    List<Resource> dexClasspathSources = inputApp.getDexClasspathResources();
-    List<Resource> dexLibrarySources = inputApp.getDexLibraryResources();
-    int numberOfFiles = dexProgramSources.size()
-        + dexLibrarySources.size() + dexClasspathSources.size();
-    if (numberOfFiles > 0) {
-      List<DexFileReader> fileReaders = new ArrayList<>(numberOfFiles);
-      int computedMinApiLevel = options.minApiLevel;
-      for (Resource input : dexProgramSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.PROGRAM, itemFactory));
-      }
-      for (Resource input : dexClasspathSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        fileReaders.add(new DexFileReader(file, ClassKind.CLASSPATH, itemFactory));
-      }
-      for (Resource input : dexLibrarySources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.LIBRARY, itemFactory));
-      }
-      options.minApiLevel = computedMinApiLevel;
-      for (DexFileReader reader : fileReaders) {
-        DexFileReader.populateIndexTables(reader);
-      }
-      // Read the DexCode items and DexProgramClass items in parallel.
-      for (DexFileReader reader : fileReaders) {
-        futures.add(executorService.submit(() -> {
-          reader.addCodeItemsTo();  // Depends on Everything for parsing.
-          reader.addClassDefsTo(builder::addClassPromise);  // Depends on Methods, Code items etc.
-        }));
-      }
-    }
-  }
-
   private int verifyOrComputeMinApiLevel(int computedMinApiLevel, DexFile file) {
     int version = file.getDexVersion();
     if (options.minApiLevel == DEFAULT_ANDROID_API) {
@@ -232,9 +158,110 @@
     }
   }
 
-  private static String getResourceClassDescriptorOrNull(Resource resource) {
-    Set<String> descriptors = resource.getClassDescriptors();
-    return (descriptors == null) || (descriptors.size() != 1)
-        ? null : descriptors.iterator().next();
+  private final class ClassReader {
+    private final ExecutorService executorService;
+    private final List<Future<?>> futures;
+    private final Closer closer;
+
+    // We use concurrent queues to collect classes
+    // since the classes can be collected concurrently.
+    private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
+    // Jar application reader to share across all class readers.
+    private final JarApplicationReader application = new JarApplicationReader(options);
+
+    ClassReader(ExecutorService executorService, List<Future<?>> futures, Closer closer) {
+      this.executorService = executorService;
+      this.futures = futures;
+      this.closer = closer;
+    }
+
+    private <T extends DexClass> void readDexSources(List<Resource> dexSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      if (dexSources.size() > 0) {
+        List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
+        int computedMinApiLevel = options.minApiLevel;
+        for (Resource input : dexSources) {
+          DexFile file = new DexFile(input.getStream(closer));
+          computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
+          fileReaders.add(new DexFileReader(file, classKind, itemFactory));
+        }
+        options.minApiLevel = computedMinApiLevel;
+        for (DexFileReader reader : fileReaders) {
+          DexFileReader.populateIndexTables(reader);
+        }
+        // Read the DexCode items and DexProgramClass items in parallel.
+        for (DexFileReader reader : fileReaders) {
+          futures.add(executorService.submit(() -> {
+            reader.addCodeItemsTo();  // Depends on Everything for parsing.
+            reader.addClassDefsTo(
+                classKind.bridgeConsumer(classes::add)); // Depends on Methods, Code items etc.
+          }));
+        }
+      }
+    }
+
+    private <T extends DexClass> void readClassSources(List<Resource> classSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      JarClassFileReader reader = new JarClassFileReader(
+          application, classKind.bridgeConsumer(classes::add));
+      for (Resource input : classSources) {
+        reader.read(DEFAULT_DEX_FILENAME, classKind, input.getStream(closer));
+      }
+    }
+
+    void readSources() throws IOException, ExecutionException {
+      readDexSources(inputApp.getDexProgramResources(), PROGRAM, programClasses);
+      readDexSources(inputApp.getDexClasspathResources(), CLASSPATH, classpathClasses);
+      readDexSources(inputApp.getDexLibraryResources(), LIBRARY, libraryClasses);
+      readClassSources(inputApp.getClassProgramResources(), PROGRAM, programClasses);
+      readClassSources(inputApp.getClassClasspathResources(), CLASSPATH, classpathClasses);
+      readClassSources(inputApp.getClassLibraryResources(), LIBRARY, libraryClasses);
+    }
+
+    private <T extends DexClass> ClassProvider<T> buildClassProvider(ClassKind classKind,
+        Queue<T> preloadedClasses, List<ClassFileResourceProvider> resourceProviders,
+        JarApplicationReader reader) {
+      List<ClassProvider<T>> providers = new ArrayList<>();
+
+      // Preloaded classes.
+      if (!preloadedClasses.isEmpty()) {
+        providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
+      }
+
+      // Class file resource providers.
+      for (ClassFileResourceProvider provider : resourceProviders) {
+        providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
+      }
+
+      // Combine if needed.
+      if (providers.isEmpty()) {
+        return null;
+      }
+      return providers.size() == 1 ? providers.get(0)
+          : ClassProvider.combine(classKind, providers);
+    }
+
+    void initializeLazyClassCollection(DexApplication.Builder builder) {
+      // Add all program classes to the builder.
+      for (DexProgramClass clazz : programClasses) {
+        builder.addProgramClass(clazz.asProgramClass());
+      }
+
+      // Create classpath class collection if needed.
+      ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
+          classpathClasses, inputApp.getClasspathResourceProviders(), application);
+      if (classpathClassProvider != null) {
+        builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
+      }
+
+      // Create library class collection if needed.
+      ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
+          libraryClasses, inputApp.getLibraryResourceProviders(), application);
+      if (libraryClassProvider != null) {
+        builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
+      }
+    }
   }
 }
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 db264ca..1636c56 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.dex;
 
+import com.android.tools.r8.dex.VirtualFile.FilePerClassDistributor;
+import com.android.tools.r8.dex.VirtualFile.FillFilesDistributor;
+import com.android.tools.r8.dex.VirtualFile.PackageMapDistributor;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationSet;
@@ -12,12 +16,15 @@
 import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.DexEncodedArray;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.naming.MinifiedNameMapPrinter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -111,13 +118,38 @@
       application.dexItemFactory.sort(namingLens);
       SortAnnotations sortAnnotations = new SortAnnotations();
       application.classes().forEach((clazz) -> clazz.addDependencies(sortAnnotations));
-      Map<Integer, VirtualFile> newFiles =
-          VirtualFile.fileSetFrom(this, packageDistribution, executorService);
 
-      // Write the dex files and the Proguard mapping file in parallel.
+      // Distribute classes into dex files.
+      VirtualFile.Distributor distributor = null;
+      if (options.outputMode == OutputMode.FilePerClass) {
+        assert packageDistribution == null :
+            "Cannot combine package distribution definition with file-per-class option.";
+        distributor = new FilePerClassDistributor(this);
+      } else if (options.minApiLevel < Constants.ANDROID_L_API
+            && options.mainDexKeepRules.isEmpty()
+            && application.mainDexList.isEmpty()) {
+        if (packageDistribution != null) {
+          throw new CompilationError("Cannot apply package distribution. Multidex is not"
+              + " supported with API level " + options.minApiLevel +"."
+              + " For API level < " + Constants.ANDROID_L_API + ", main dex classes list or"
+              + " rules must be specified.");
+        }
+        distributor = new VirtualFile.MonoDexDistributor(this);
+      } else if (packageDistribution != null) {
+        assert !options.minimalMainDex :
+            "Cannot combine package distribution definition with minimal-main-dex option.";
+        distributor = new PackageMapDistributor(this, packageDistribution, executorService);
+      } else {
+        distributor = new FillFilesDistributor(this, options.minimalMainDex);
+      }
+      Map<Integer, VirtualFile> newFiles = distributor.run();
+
+      // Write the dex files and the Proguard mapping file in parallel. Use a linked hash map
+      // as the order matters when addDexProgramData is called below.
       LinkedHashMap<VirtualFile, Future<byte[]>> dexDataFutures = new LinkedHashMap<>();
-      for (Integer index : newFiles.keySet()) {
-        VirtualFile newFile = newFiles.get(index);
+      for (int i = 0; i < newFiles.size(); i++) {
+        VirtualFile newFile = newFiles.get(i);
+        assert newFile.getId() == i;
         if (!newFile.isEmpty()) {
           dexDataFutures.put(newFile, executorService.submit(() -> writeDexFile(newFile)));
         }
@@ -141,6 +173,10 @@
       if (proguardSeedsData != null) {
         builder.setProguardSeedsData(proguardSeedsData);
       }
+      byte[] mainDexList = writeMainDexList();
+      if (mainDexList != null) {
+        builder.setMainDexListData(mainDexList);
+      }
       return builder.build();
     } finally {
       application.timing.end();
@@ -177,4 +213,22 @@
     }
     return null;
   }
+
+  private String mapMainDexListName(DexType type) {
+    return DescriptorUtils.descriptorToJavaType(namingLens.lookupDescriptor(type).toString())
+        .replace('.', '/') + ".class";
+  }
+
+  private byte[] writeMainDexList() throws IOException {
+    if (application.mainDexList.isEmpty()) {
+      return null;
+    }
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    PrintWriter writer = new PrintWriter(bytes);
+    application.mainDexList.forEach(
+        type -> writer.println(mapMainDexListName(type))
+    );
+    writer.flush();
+    return bytes.toByteArray();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index 201fc82..3ea63bb 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -10,6 +10,7 @@
 
   public static final int ANDROID_O_API = 26;
   public static final int ANDROID_N_API = 24;
+  public static final int ANDROID_L_API = 21;
   public static final int ANDROID_K_API = 19;
   public static final int DEFAULT_ANDROID_API = 1;
 
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 07fce3a..0e89d4a 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -23,11 +23,10 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
@@ -55,6 +54,19 @@
 
 public class VirtualFile {
 
+  // The fill strategy determine how to distribute classes into dex files.
+  enum FillStrategy {
+    // Only put classes matches by the main dex rules into the first dex file. Distribute remaining
+    // classes in additional dex files filling each dex file as much as possible.
+    MINIMAL_MAIN_DEX,
+    // Distribute classes in as few dex files as possible filling each dex file as much as possible.
+    FILL_MAX,
+    // Distribute classes keeping some space for future growth. This is mainly useful together with
+    // the package map distribution.
+    LEAVE_SPACE_FOR_GROWTH,
+    // TODO(sgjesse): Does "minimal main dex" combined with "leave space for growth" make sense?
+  }
+
   private static final int MAX_ENTRIES = (Short.MAX_VALUE << 1) + 1;
   /**
    * When distributing classes across files we aim to leave some space. The amount of space left is
@@ -72,6 +84,10 @@
     this.transaction = new IndexedItemTransaction(indexedItems, namingLens);
   }
 
+  public int getId() {
+    return id;
+  }
+
   public Set<String> getClassDescriptors() {
     Set<String> classDescriptors = new HashSet<>();
     for (DexProgramClass clazz : indexedItems.classes) {
@@ -81,119 +97,6 @@
     return classDescriptors;
   }
 
-  public static Map<Integer, VirtualFile> fileSetFrom(
-      ApplicationWriter writer,
-      PackageDistribution packageDistribution,
-      ExecutorService executorService)
-      throws ExecutionException, IOException {
-    // Strategy for distributing classes for write out:
-    // 1. Place all files in the package distribution file in the proposed files (if any).
-    // 2. Place the remaining files based on their packages in sorted order.
-    DexApplication application = writer.application;
-    InternalOptions options = writer.options;
-    Map<Integer, VirtualFile> nameToFileMap = new LinkedHashMap<>();
-
-    if (options.outputMode == OutputMode.FilePerClass) {
-      assert packageDistribution == null :
-          "Cannot combine package distribution definition with file-per-class option.";
-      // Assign dedicated virtual files for all program classes.
-      for (DexProgramClass clazz : application.classes()) {
-        VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
-        nameToFileMap.put(nameToFileMap.size(), file);
-        file.addClass(clazz);
-        file.commitTransaction();
-      }
-      return nameToFileMap;
-    }
-
-    if (packageDistribution != null) {
-      int maxReferencedIndex = packageDistribution.maxReferencedIndex();
-      for (int index = 0; index <= maxReferencedIndex; index++) {
-        VirtualFile file = new VirtualFile(index, writer.namingLens);
-        nameToFileMap.put(index, file);
-      }
-    } else {
-      // If we had no map we default to 1 file, the package populator will add more if needed.
-      nameToFileMap.put(0, new VirtualFile(0, writer.namingLens));
-    }
-    Set<DexProgramClass> classes = Sets.newHashSet(application.classes());
-
-    // Compute the original names.
-    Map<DexProgramClass, String> originalNames = computeOriginalNameMapping(classes,
-        application.getProguardMap());
-
-    if (application.mainDexList != null) {
-      VirtualFile mainDexFile = nameToFileMap.get(0);
-      for (DexType type : application.mainDexList) {
-        DexClass clazz = application.definitionFor(type);
-        if (clazz != null && clazz.isProgramClass()) {
-          DexProgramClass programClass = (DexProgramClass) clazz;
-          mainDexFile.addClass(programClass);
-          if (mainDexFile.isFull()) {
-            throw new CompilationError("Cannot fit requested classes in main-dex file.");
-          }
-          classes.remove(programClass);
-        } else {
-          System.out.println(
-              "WARNING: Application does not contain `"
-                  + type.toDescriptorString()
-                  + "` as referenced in main-dex-list.");
-        }
-        mainDexFile.commitTransaction();
-      }
-    }
-
-    // Sort the classes based on the original names.
-    // This with make classes from the same package be adjacent.
-    classes = sortClassesByPackage(classes, originalNames);
-
-    Set<String> usedPrefixes = null;
-    if (packageDistribution != null) {
-      ArrayList<Future<List<DexProgramClass>>> futures = new ArrayList<>(nameToFileMap.size());
-      usedPrefixes = packageDistribution.getFiles();
-      for (VirtualFile file : nameToFileMap.values()) {
-        PackageMapPopulator populator =
-            new PackageMapPopulator(file, classes, packageDistribution, originalNames);
-        futures.add(executorService.submit(populator));
-      }
-      ThreadUtils.awaitFutures(futures).forEach(classes::removeAll);
-    }
-
-    // TODO(zerny): Add the package map to AndroidApp and refactor its generation.
-    Path newPackageMap = Paths.get("package.map");
-    Map<String, Integer> newAssignments;
-    if (classes.isEmpty()) {
-      newAssignments = Collections.emptyMap();
-    } else {
-      newAssignments =
-          new PackageSplitPopulator(
-              nameToFileMap, classes, originalNames, usedPrefixes, application.dexItemFactory,
-              options, writer.namingLens)
-              .call();
-      if (!newAssignments.isEmpty() && nameToFileMap.size() > 1) {
-        if (packageDistribution == null) {
-          System.out.println(" * Consider using a package map to improve patch sizes.");
-        } else {
-          System.err.println(" * The used package map is missing entries. The following default "
-              + "mappings have been used:");
-          Writer output = new OutputStreamWriter(System.err);
-          for (Entry<String, Integer> entry : newAssignments.entrySet()) {
-            output.write("    ");
-            PackageDistribution.formatEntry(entry, output);
-            output.write("\n");
-          }
-          output.flush();
-          System.err.println(" * Consider updating the map.");
-        }
-      }
-    }
-    if (packageDistribution != null || nameToFileMap.size() > 1) {
-      System.out.println(" - " + newPackageMap.toString());
-      PackageDistribution.writePackageToFileMap(newPackageMap, newAssignments, packageDistribution);
-    }
-    return nameToFileMap;
-  }
-
   public static String deriveCommonPrefixAndSanityCheck(List<String> fileNames) {
     Iterator<String> nameIterator = fileNames.iterator();
     String first = nameIterator.next();
@@ -228,38 +131,6 @@
     return originalNames;
   }
 
-  private static TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
-      Map<DexProgramClass, String> originalNames) {
-    TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
-        (DexProgramClass a, DexProgramClass b) -> {
-      String originalA = originalNames.get(a);
-      String originalB = originalNames.get(b);
-      int indexA = originalA.lastIndexOf('.');
-      int indexB = originalB.lastIndexOf('.');
-      if (indexA == -1 && indexB == -1) {
-        // Empty package, compare the class names.
-        return originalA.compareTo(originalB);
-      }
-      if (indexA == -1) {
-        // Empty package name comes first.
-        return -1;
-      }
-      if (indexB == -1) {
-        // Empty package name comes first.
-        return 1;
-      }
-      String prefixA = originalA.substring(0, indexA);
-      String prefixB = originalB.substring(0, indexB);
-      int result = prefixA.compareTo(prefixB);
-      if (result != 0) {
-        return result;
-      }
-      return originalA.compareTo(originalB);
-    });
-    sortedClasses.addAll(classes);
-    return sortedClasses;
-  }
-
   private static String extractPrefixToken(int prefixLength, String className, boolean addStar) {
     int index = 0;
     int lastIndex = 0;
@@ -303,11 +174,11 @@
     return isFull(transaction.getNumberOfMethods(), transaction.getNumberOfFields(), MAX_ENTRIES);
   }
 
-  private boolean isFilledEnough(InternalOptions options) {
+  private boolean isFilledEnough(FillStrategy fillStrategy) {
     return isFull(
         transaction.getNumberOfMethods(),
         transaction.getNumberOfFields(),
-        options.fillDexFiles ? MAX_ENTRIES : MAX_PREFILL_ENTRIES);
+        fillStrategy == FillStrategy.FILL_MAX ? MAX_ENTRIES : MAX_PREFILL_ENTRIES);
   }
 
   public void abortTransaction() {
@@ -326,6 +197,240 @@
     return indexedItems.classes;
   }
 
+  public abstract static class Distributor {
+    protected final DexApplication application;
+    protected final ApplicationWriter writer;
+    protected final Map<Integer, VirtualFile> nameToFileMap = new HashMap<>();
+
+    public Distributor(ApplicationWriter writer) {
+      this.application = writer.application;
+      this.writer = writer;
+    }
+
+    public abstract Map<Integer, VirtualFile> run() throws ExecutionException, IOException;
+  }
+
+  public static class FilePerClassDistributor extends Distributor {
+
+    public FilePerClassDistributor(ApplicationWriter writer) {
+      super(writer);
+    }
+
+    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+      // Assign dedicated virtual files for all program classes.
+      for (DexProgramClass clazz : application.classes()) {
+        VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
+        nameToFileMap.put(nameToFileMap.size(), file);
+        file.addClass(clazz);
+        file.commitTransaction();
+      }
+      return nameToFileMap;
+    }
+  }
+
+  public abstract static class DistributorBase extends Distributor {
+    protected Set<DexProgramClass> classes;
+    protected Map<DexProgramClass, String> originalNames;
+
+    public DistributorBase(ApplicationWriter writer) {
+      super(writer);
+
+      classes = Sets.newHashSet(application.classes());
+      originalNames = computeOriginalNameMapping(classes, application.getProguardMap());
+    }
+
+    protected void fillForMainDexList(Set<DexProgramClass> classes) {
+      if (application.mainDexList != null) {
+        VirtualFile mainDexFile = nameToFileMap.get(0);
+        for (DexType type : application.mainDexList) {
+          DexClass clazz = application.definitionFor(type);
+          if (clazz != null && clazz.isProgramClass()) {
+            DexProgramClass programClass = (DexProgramClass) clazz;
+            mainDexFile.addClass(programClass);
+            if (mainDexFile.isFull()) {
+              throw new CompilationError("Cannot fit requested classes in main-dex file.");
+            }
+            classes.remove(programClass);
+          } else {
+            System.out.println(
+                "WARNING: Application does not contain `"
+                    + type.toSourceString()
+                    + "` as referenced in main-dex-list.");
+          }
+          mainDexFile.commitTransaction();
+        }
+      }
+    }
+
+    TreeSet<DexProgramClass> sortClassesByPackage(Set<DexProgramClass> classes,
+        Map<DexProgramClass, String> originalNames) {
+      TreeSet<DexProgramClass> sortedClasses = new TreeSet<>(
+          (DexProgramClass a, DexProgramClass b) -> {
+            String originalA = originalNames.get(a);
+            String originalB = originalNames.get(b);
+            int indexA = originalA.lastIndexOf('.');
+            int indexB = originalB.lastIndexOf('.');
+            if (indexA == -1 && indexB == -1) {
+              // Empty package, compare the class names.
+              return originalA.compareTo(originalB);
+            }
+            if (indexA == -1) {
+              // Empty package name comes first.
+              return -1;
+            }
+            if (indexB == -1) {
+              // Empty package name comes first.
+              return 1;
+            }
+            String prefixA = originalA.substring(0, indexA);
+            String prefixB = originalB.substring(0, indexB);
+            int result = prefixA.compareTo(prefixB);
+            if (result != 0) {
+              return result;
+            }
+            return originalA.compareTo(originalB);
+          });
+      sortedClasses.addAll(classes);
+      return sortedClasses;
+    }
+  }
+
+  public static class FillFilesDistributor extends DistributorBase {
+    private final FillStrategy fillStrategy;
+
+    public FillFilesDistributor(ApplicationWriter writer, boolean minimalMainDex) {
+      super(writer);
+      this.fillStrategy = minimalMainDex ? FillStrategy.MINIMAL_MAIN_DEX : FillStrategy.FILL_MAX;
+    }
+
+    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+      // Strategy for distributing classes for write out:
+      // 1. Place the remaining files based on their packages in sorted order.
+
+      // Start with 1 file. The package populator will add more if needed.
+      nameToFileMap.put(0, new VirtualFile(0, writer.namingLens));
+
+      // First fill required classes into the main dex file.
+      fillForMainDexList(classes);
+
+      // Sort the remaining classes based on the original names.
+      // This with make classes from the same package be adjacent.
+      classes = sortClassesByPackage(classes, originalNames);
+
+      new PackageSplitPopulator(
+          nameToFileMap, classes, originalNames, null, application.dexItemFactory,
+          fillStrategy, writer.namingLens)
+          .call();
+      return nameToFileMap;
+    }
+  }
+
+  public static class MonoDexDistributor extends DistributorBase {
+    public MonoDexDistributor(ApplicationWriter writer) {
+      super(writer);
+    }
+
+    @Override
+    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+      VirtualFile mainDexFile = new VirtualFile(0, writer.namingLens);
+      nameToFileMap.put(0, mainDexFile);
+
+      for (DexProgramClass programClass : classes) {
+        mainDexFile.addClass(programClass);
+        if (mainDexFile.isFull()) {
+          throw new CompilationError("Cannot fit all classes in a single dex file.");
+        }
+      }
+      mainDexFile.commitTransaction();
+      return nameToFileMap;
+    }
+  }
+
+  public static class PackageMapDistributor extends DistributorBase {
+    private final PackageDistribution packageDistribution;
+    private final ExecutorService executorService;
+
+    public PackageMapDistributor(
+        ApplicationWriter writer,
+        PackageDistribution packageDistribution,
+        ExecutorService executorService) {
+      super(writer);
+      this.packageDistribution = packageDistribution;
+      this.executorService = executorService;
+    }
+
+    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+      // Strategy for distributing classes for write out:
+      // 1. Place all files in the package distribution file in the proposed files (if any).
+      // 2. Place the remaining files based on their packages in sorted order.
+
+      int maxReferencedIndex = packageDistribution.maxReferencedIndex();
+      for (int index = 0; index <= maxReferencedIndex; index++) {
+        VirtualFile file = new VirtualFile(index, writer.namingLens);
+        nameToFileMap.put(index, file);
+      }
+
+      // First fill required classes into the main dex file.
+      fillForMainDexList(classes);
+
+      // Sort the remaining classes based on the original names.
+      // This with make classes from the same package be adjacent.
+      classes = sortClassesByPackage(classes, originalNames);
+
+      Set<String> usedPrefixes = fillForDistribution(classes, originalNames);
+
+      // TODO(zerny): Add the package map to AndroidApp and refactor its generation.
+      Map<String, Integer> newAssignments;
+      if (classes.isEmpty()) {
+        newAssignments = Collections.emptyMap();
+      } else {
+        newAssignments =
+            new PackageSplitPopulator(
+                nameToFileMap, classes, originalNames, usedPrefixes, application.dexItemFactory,
+                FillStrategy.LEAVE_SPACE_FOR_GROWTH, writer.namingLens)
+                .call();
+        if (!newAssignments.isEmpty() && nameToFileMap.size() > 1) {
+          System.err.println(" * The used package map is missing entries. The following default "
+              + "mappings have been used:");
+          writeAssignments(newAssignments, new OutputStreamWriter(System.err));
+          System.err.println(" * Consider updating the map.");
+        }
+      }
+
+      Path newPackageMap = Paths.get("package.map");
+      System.out.println(" - " + newPackageMap.toString());
+      PackageDistribution.writePackageToFileMap(newPackageMap, newAssignments, packageDistribution);
+
+      return nameToFileMap;
+    }
+
+    private Set<String> fillForDistribution(Set<DexProgramClass> classes,
+        Map<DexProgramClass, String> originalNames) throws ExecutionException {
+      Set<String> usedPrefixes = null;
+      if (packageDistribution != null) {
+        ArrayList<Future<List<DexProgramClass>>> futures = new ArrayList<>(nameToFileMap.size());
+        usedPrefixes = packageDistribution.getFiles();
+        for (VirtualFile file : nameToFileMap.values()) {
+          PackageMapPopulator populator =
+              new PackageMapPopulator(file, classes, packageDistribution, originalNames);
+          futures.add(executorService.submit(populator));
+        }
+        ThreadUtils.awaitFutures(futures).forEach(classes::removeAll);
+      }
+      return usedPrefixes;
+    }
+
+    private void writeAssignments(Map<String, Integer> assignments, Writer output)
+        throws IOException{
+      for (Entry<String, Integer> entry : assignments.entrySet()) {
+        output.write("    ");
+        PackageDistribution.formatEntry(entry, output);
+        output.write("\n");
+      }
+      output.flush();
+    }
+  }
+
   private static class VirtualFileIndexedItemCollection implements IndexedItemCollection {
 
     final int id;
@@ -575,6 +680,7 @@
       this.originalNames = originalNames;
     }
 
+    @Override
     public List<DexProgramClass> call() {
       String currentPrefix = null;
       int currentFileId = -1;
@@ -598,9 +704,9 @@
           inserted.add(clazz);
         }
         if (file.isFull()) {
-          throw new RuntimeException(
+          throw new CompilationError(
               "Cannot fit package " + currentPrefix
-                  + " in requested dex file, consider to remove mapping.");
+                  + " in requested dex file, consider removing mapping.");
         }
       }
       file.commitTransaction();
@@ -643,6 +749,74 @@
   }
 
   /**
+   * Helper class to cycle through the set of virtual files.
+   *
+   * Iteration starts at the first file and iterates through all files.
+   *
+   * When {@link VirtualFileCycler#restart()} is called iteration of all files is restarted at the
+   * current file.
+   *
+   * If the fill strategy indicate that the main dex file should be minimal, then the main dex file
+   * will not be part of the iteration.
+   */
+  private static class VirtualFileCycler {
+    private Map<Integer, VirtualFile> files;
+    private final NamingLens namingLens;
+    private final FillStrategy fillStrategy;
+
+    private int nextFileId;
+    private Iterator<VirtualFile> allFilesCyclic;
+    private Iterator<VirtualFile> activeFiles;
+
+    VirtualFileCycler(Map<Integer, VirtualFile> files, NamingLens namingLens,
+        FillStrategy fillStrategy) {
+      this.files = files;
+      this.namingLens = namingLens;
+      this.fillStrategy = fillStrategy;
+
+      nextFileId = files.size();
+      if (fillStrategy == FillStrategy.MINIMAL_MAIN_DEX) {
+        // The main dex file is filtered out, so ensure at least one file for the remaining
+        // classes
+        files.put(nextFileId, new VirtualFile(nextFileId, namingLens));
+        this.files = Maps.filterKeys(files, key -> key != 0);
+        nextFileId++;
+      }
+
+      reset();
+    }
+
+    private void reset() {
+      allFilesCyclic = Iterators.cycle(files.values());
+      restart();
+    }
+
+    boolean hasNext() {
+      return activeFiles.hasNext();
+    }
+
+    VirtualFile next() {
+      VirtualFile next = activeFiles.next();
+      assert fillStrategy != FillStrategy.MINIMAL_MAIN_DEX || next.getId() != 0;
+      return next;
+    }
+
+    // Start a new iteration over all files, starting at the current one.
+    void restart() {
+      activeFiles = Iterators.limit(allFilesCyclic, files.size());
+    }
+
+    VirtualFile addFile() {
+      VirtualFile newFile = new VirtualFile(nextFileId, namingLens);
+      files.put(nextFileId, newFile);
+      nextFileId++;
+
+      reset();
+      return newFile;
+    }
+  }
+
+  /**
    * Distributes the given classes over the files in package order.
    *
    * <p>The populator avoids package splits. Big packages are split into subpackages if their size
@@ -666,13 +840,12 @@
      */
     private static final int MIN_FILL_FACTOR = 5;
 
-    private final Map<Integer, VirtualFile> files;
     private final List<DexProgramClass> classes;
     private final Map<DexProgramClass, String> originalNames;
     private final Set<String> previousPrefixes;
     private final DexItemFactory dexItemFactory;
-    private final InternalOptions options;
-    private final NamingLens namingLens;
+    private final FillStrategy fillStrategy;
+    private final VirtualFileCycler cycler;
 
     PackageSplitPopulator(
         Map<Integer, VirtualFile> files,
@@ -680,30 +853,28 @@
         Map<DexProgramClass, String> originalNames,
         Set<String> previousPrefixes,
         DexItemFactory dexItemFactory,
-        InternalOptions options,
+        FillStrategy fillStrategy,
         NamingLens namingLens) {
-      this.files = files;
       this.classes = new ArrayList<>(classes);
       this.originalNames = originalNames;
       this.previousPrefixes = previousPrefixes;
       this.dexItemFactory = dexItemFactory;
-      this.options = options;
-      this.namingLens = namingLens;
+      this.fillStrategy = fillStrategy;
+      this.cycler = new VirtualFileCycler(files, namingLens, fillStrategy);
     }
 
     private String getOriginalName(DexProgramClass clazz) {
       return originalNames != null ? originalNames.get(clazz) : clazz.toString();
     }
 
+    @Override
     public Map<String, Integer> call() throws IOException {
-      Iterator<VirtualFile> allFilesCyclic = Iterators.cycle(files.values());
-      Iterator<VirtualFile> activeFiles = Iterators.limit(allFilesCyclic, files.size());
       int prefixLength = MINIMUM_PREFIX_LENGTH;
       int transactionStartIndex = 0;
       int fileStartIndex = 0;
       String currentPrefix = null;
       Map<String, Integer> newPackageAssignments = new LinkedHashMap<>();
-      VirtualFile current = activeFiles.next();
+      VirtualFile current = cycler.next();
       List<DexProgramClass> nonPackageClasses = new ArrayList<>();
       for (int classIndex = 0; classIndex < classes.size(); classIndex++) {
         DexProgramClass clazz = classes.get(classIndex);
@@ -711,8 +882,8 @@
         if (!PackageMapPopulator.coveredByPrefix(originalName, currentPrefix)) {
           if (currentPrefix != null) {
             current.commitTransaction();
-            // Reset the iterator to again iterate over all files, starting with the current one.
-            activeFiles = Iterators.limit(allFilesCyclic, files.size());
+            // Reset the cycler to again iterate over all files, starting with the current one.
+            cycler.restart();
             assert !newPackageAssignments.containsKey(currentPrefix);
             newPackageAssignments.put(currentPrefix, current.id);
             // Try to reduce the prefix length if possible. Only do this on a successful commit.
@@ -750,7 +921,7 @@
           nonPackageClasses.add(clazz);
           continue;
         }
-        if (current.isFilledEnough(options) || current.isFull()) {
+        if (current.isFilledEnough(fillStrategy) || current.isFull()) {
           current.abortTransaction();
           // We allow for a final rollback that has at most 20% of classes in it.
           // This is a somewhat random number that was empirically chosen.
@@ -762,7 +933,7 @@
             // The idea is that we do not increase the number of files, so it has to fit
             // somewhere.
             fileStartIndex = transactionStartIndex;
-            if (!activeFiles.hasNext()) {
+            if (!cycler.hasNext()) {
               // Special case where we simply will never be able to fit the current package into
               // one dex file. This is currently the case for Strings in jumbo tests, see:
               // b/33227518
@@ -773,11 +944,9 @@
                 transactionStartIndex = classIndex + 1;
               }
               // All files are filled up to the 20% mark.
-              files.put(files.size(), new VirtualFile(files.size(), namingLens));
-              allFilesCyclic = Iterators.cycle(files.values());
-              activeFiles = Iterators.limit(allFilesCyclic, files.size());
+              cycler.addFile();
             }
-            current = activeFiles.next();
+            current = cycler.next();
           }
           currentPrefix = null;
           // Go back to previous start index.
@@ -791,24 +960,25 @@
         newPackageAssignments.put(currentPrefix, current.id);
       }
       if (nonPackageClasses.size() > 0) {
-        addNonPackageClasses(Iterators.limit(allFilesCyclic, files.size()), nonPackageClasses);
+        addNonPackageClasses(cycler, nonPackageClasses);
       }
       return newPackageAssignments;
     }
 
-    private void addNonPackageClasses(Iterator<VirtualFile> activeFiles,
-        List<DexProgramClass> nonPackageClasses) throws IOException {
+    private void addNonPackageClasses(
+        VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
+      cycler.restart();
       VirtualFile current;
-      current = activeFiles.next();
+      current = cycler.next();
       for (DexProgramClass clazz : nonPackageClasses) {
-        if (current.isFilledEnough(options)) {
-          current = getVirtualFile(activeFiles);
+        if (current.isFilledEnough(fillStrategy)) {
+          current = getVirtualFile(cycler);
         }
         current.addClass(clazz);
         while (current.isFull()) {
           // This only happens if we have a huge class, that takes up more than 20% of a dex file.
           current.abortTransaction();
-          current = getVirtualFile(activeFiles);
+          current = getVirtualFile(cycler);
           boolean wasEmpty = current.isEmpty();
           current.addClass(clazz);
           if (wasEmpty && current.isFull()) {
@@ -820,12 +990,12 @@
       }
     }
 
-    private VirtualFile getVirtualFile(Iterator<VirtualFile> activeFiles) throws IOException {
+    private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
       VirtualFile current = null;
-      while (activeFiles.hasNext() && (current = activeFiles.next()).isFilledEnough(options)) {}
-      if (current == null || current.isFilledEnough(options)) {
-        current = new VirtualFile(files.size(), namingLens);
-        files.put(files.size(), current);
+      while (cycler.hasNext()
+          && (current = cycler.next()).isFilledEnough(fillStrategy)) {}
+      if (current == null || current.isFilledEnough(fillStrategy)) {
+        current = cycler.addFile();
       }
       return current;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index a558b3e..a5c8ff8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -40,9 +40,8 @@
 
   private Map<Descriptor, KeyedDexItem> computeDefinitions(DexType type) {
     Builder<Descriptor, KeyedDexItem> builder = ImmutableMap.builder();
-    DexClassPromise promise = app.definitionFor(type);
-    if (promise != null) {
-      DexClass clazz = promise.get();
+    DexClass clazz = app.definitionFor(type);
+    if (clazz != null) {
       registerDefinitions(builder, clazz.directMethods());
       registerDefinitions(builder, clazz.virtualMethods());
       registerDefinitions(builder, clazz.instanceFields());
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 9330fdd..9e3e118 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -21,7 +21,7 @@
 
   public AppInfoWithSubtyping(DexApplication application) {
     super(application);
-    populateSubtypeMap(application.getClassMap(), application.dexItemFactory);
+    populateSubtypeMap(application.getFullClassMap(), application.dexItemFactory);
   }
 
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous) {
@@ -33,7 +33,7 @@
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous, GraphLense lense) {
     super(previous, lense);
     // Recompute subtype map if we have modified the graph.
-    populateSubtypeMap(previous.app.getClassMap(), dexItemFactory);
+    populateSubtypeMap(previous.app.getFullClassMap(), dexItemFactory);
   }
 
   public Set<DexType> getMissingClasses() {
@@ -84,22 +84,17 @@
     }
   }
 
-  private void populateSubtypeMap(
-      Map<DexType, DexClassPromise> classes, DexItemFactory dexItemFactory) {
+  private void populateSubtypeMap(Map<DexType, DexClass> classes, DexItemFactory dexItemFactory) {
     dexItemFactory.clearSubtypeInformation();
     dexItemFactory.objectType.tagAsSubtypeRoot();
     Hashtable<DexType, Set<DexType>> map = new Hashtable<>();
-    Function<DexType, DexClass> typeToClass = type -> {
-      DexClassPromise promise = classes.get(type);
-      return promise == null ? null : promise.get();
-    };
-    for (Map.Entry<DexType, DexClassPromise> entry : classes.entrySet()) {
-      populateAllSuperTypes(map, entry.getKey(), entry.getValue().get(), typeToClass);
+    for (Map.Entry<DexType, DexClass> entry : classes.entrySet()) {
+      populateAllSuperTypes(map, entry.getKey(), entry.getValue(), classes::get);
     }
     for (Map.Entry<DexType, Set<DexType>> entry : map.entrySet()) {
       subtypeMap.put(entry.getKey(), ImmutableSet.copyOf(entry.getValue()));
     }
-    assert DexType.validateLevelsAreCorrect(typeToClass, dexItemFactory);
+    assert DexType.validateLevelsAreCorrect(classes::get, dexItemFactory);
   }
 
   // For mapping invoke virtual instruction to target methods.
diff --git a/src/main/java/com/android/tools/r8/graph/ClassKind.java b/src/main/java/com/android/tools/r8/graph/ClassKind.java
index 6f53a40..9298ba5 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassKind.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassKind.java
@@ -1,12 +1,14 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.Resource;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Kind of the application class. Can be program, classpath or library. */
 public enum ClassKind {
-  PROGRAM(DexProgramClass::new),
-  CLASSPATH(DexClasspathClass::new),
-  LIBRARY(DexLibraryClass::new);
+  PROGRAM(DexProgramClass::new, DexClass::isProgramClass),
+  CLASSPATH(DexClasspathClass::new, DexClass::isClasspathClass),
+  LIBRARY(DexLibraryClass::new, DexClass::isLibraryClass);
 
   private interface Factory {
     DexClass create(DexType type, Resource.Kind origin, DexAccessFlags accessFlags,
@@ -17,9 +19,11 @@
   }
 
   private final Factory factory;
+  private final Predicate<DexClass> check;
 
-  ClassKind(Factory factory) {
+  ClassKind(Factory factory, Predicate<DexClass> check) {
     this.factory = factory;
+    this.check = check;
   }
 
   public DexClass create(
@@ -30,4 +34,16 @@
     return factory.create(type, origin, accessFlags, superType, interfaces, sourceFile,
         annotations, staticFields, instanceFields, directMethods, virtualMethods);
   }
+
+  public boolean isOfKind(DexClass clazz) {
+    return check.test(clazz);
+  }
+
+  public <T extends DexClass> Consumer<DexClass> bridgeConsumer(Consumer<T> consumer) {
+    return clazz -> {
+      assert isOfKind(clazz);
+      @SuppressWarnings("unchecked") T specialized = (T) clazz;
+      consumer.accept(specialized);
+    };
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index 04c01be..dca5e02 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -19,7 +19,7 @@
 
   public abstract String toString();
 
-  public abstract String toString(ClassNameMapper naming);
+  public abstract String toString(DexEncodedMethod method, ClassNameMapper naming);
 
   public boolean isDexCode() {
     return false;
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index acbc205..eea0169 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -6,16 +6,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
+import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.io.ByteArrayOutputStream;
@@ -26,25 +23,20 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Hashtable;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 public class DexApplication {
 
-  // Maps type into class promise, may be used concurrently.
-  private final ImmutableMap<DexType, DexClassPromise> classMap;
-
-  // Lazily loaded classes.
-  //
-  // Note that this collection is autonomous and may be used in several
-  // different applications. Particularly, it is the case when one
-  // application is being build based on another one. Among others,
-  // it will have an important side-effect: class conflict resolution,
-  // generated errors in particular, may be different in lazy scenario.
-  private final LazyClassCollection lazyClassCollection;
+  // Maps type into class, may be used concurrently.
+  private ProgramClassCollection programClasses;
+  private ClasspathClassCollection classpathClasses;
+  private LibraryClassCollection libraryClasses;
 
   public final ImmutableSet<DexType> mainDexList;
 
@@ -60,67 +52,97 @@
   /** Constructor should only be invoked by the DexApplication.Builder. */
   private DexApplication(
       ClassNameMapper proguardMap,
-      ImmutableMap<DexType, DexClassPromise> classMap,
-      LazyClassCollection lazyClassCollection,
+      ProgramClassCollection programClasses,
+      ClasspathClassCollection classpathClasses,
+      LibraryClassCollection libraryClasses,
       ImmutableSet<DexType> mainDexList,
       DexItemFactory dexItemFactory,
       DexString highestSortingString,
       Timing timing) {
+    assert programClasses != null;
     this.proguardMap = proguardMap;
-    this.lazyClassCollection = lazyClassCollection;
+    this.programClasses = programClasses;
+    this.classpathClasses = classpathClasses;
+    this.libraryClasses = libraryClasses;
     this.mainDexList = mainDexList;
-    this.classMap = classMap;
     this.dexItemFactory = dexItemFactory;
     this.highestSortingString = highestSortingString;
     this.timing = timing;
   }
 
-  ImmutableMap<DexType, DexClassPromise> getClassMap() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    return classMap;
+  /** Force load all classes and return type -> class map containing all the classes */
+  public Map<DexType, DexClass> getFullClassMap() {
+    return forceLoadAllClasses();
   }
 
-  public Iterable<DexProgramClass> classes() {
-    List<DexProgramClass> result = new ArrayList<>();
-    // Note: we ignore lazy class collection because it
-    // is never supposed to be used for program classes.
-    for (DexClassPromise promise : classMap.values()) {
-      if (promise.isProgramClass()) {
-        result.add(promise.get().asProgramClass());
-      }
-    }
-    return result;
+  // Reorder classes randomly. Note that the order of classes in program or library
+  // class collections should not matter for compilation of valid code and when running
+  // with assertions enabled we reorder the classes randomly to catch possible issues.
+  // Also note that the order may add to non-determinism in reporting errors for invalid
+  // code, but this non-determinism exists even with the same order of classes since we
+  // may process classes concurrently and fail-fast on the first error.
+  private <T> boolean reorderClasses(List<T> classes) {
+    Collections.shuffle(classes);
+    return true;
   }
 
-  public Iterable<DexLibraryClass> libraryClasses() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    List<DexLibraryClass> result = new ArrayList<>();
-    for (DexClassPromise promise : classMap.values()) {
-      if (promise.isLibraryClass()) {
-        result.add(promise.get().asLibraryClass());
+  public List<DexProgramClass> classes() {
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
+    assert reorderClasses(classes);
+    return classes;
+  }
+
+  public List<DexLibraryClass> libraryClasses() {
+    assert classpathClasses == null : "Operation is not supported.";
+    Map<DexType, DexClass> classMap = forceLoadAllClasses();
+    List<DexLibraryClass> classes = new ArrayList<>();
+    for (DexClass clazz : classMap.values()) {
+      if (clazz.isLibraryClass()) {
+        classes.add(clazz.asLibraryClass());
       }
     }
-    return result;
+    assert reorderClasses(classes);
+    return classes;
+  }
+
+  private Map<DexType, DexClass> forceLoadAllClasses() {
+    Map<DexType, DexClass> loaded = new IdentityHashMap<>();
+
+    // program classes are supposed to be loaded, but force-loading them is no-op.
+    programClasses.forceLoad(type -> true);
+    programClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+
+    if (classpathClasses != null) {
+      classpathClasses.forceLoad(type -> !loaded.containsKey(type));
+      classpathClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    if (libraryClasses != null) {
+      libraryClasses.forceLoad(type -> !loaded.containsKey(type));
+      libraryClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    return loaded;
   }
 
   public DexClass definitionFor(DexType type) {
-    DexClassPromise promise = classMap.get(type);
-    // In presence of lazy class collection we also reach out to it
-    // as well unless the class found is already a program class.
-    if (lazyClassCollection != null && (promise == null || !promise.isProgramClass())) {
-      promise = lazyClassCollection.get(type, promise);
+    DexClass clazz = programClasses.get(type);
+    if (clazz == null && classpathClasses != null) {
+      clazz = classpathClasses.get(type);
     }
-    return promise == null ? null : promise.get();
+    if (clazz == null && libraryClasses != null) {
+      clazz = libraryClasses.get(type);
+    }
+    return clazz;
   }
 
   public DexProgramClass programDefinitionFor(DexType type) {
-    DexClassPromise promise = classMap.get(type);
-    // Don't bother about lazy class collection, it should never load program classes.
-    return (promise == null || !promise.isProgramClass()) ? null : promise.get().asProgramClass();
+    DexClass clazz = programClasses.get(type);
+    return clazz == null ? null : clazz.asProgramClass();
   }
 
   public String toString() {
-    return "Application (classes #" + classMap.size() + ")";
+    return "Application (" + programClasses + "; " + classpathClasses + "; " + libraryClasses + ")";
   }
 
   public ClassNameMapper getProguardMap() {
@@ -152,7 +174,7 @@
         ps.println("Bytecode for");
         ps.println("Class: '" + clazzName + "'");
         ps.println("Method: '" + methodName + "':");
-        ps.println(method.getCode().toString(naming));
+        ps.println(method.getCode().toString(method, naming));
       } catch (IOException e) {
         e.printStackTrace();
       } finally {
@@ -170,7 +192,7 @@
    * <p>If no directory is provided everything is written to System.out.
    */
   public void disassemble(Path outputDir, InternalOptions options) {
-    for (DexClass clazz : classes()) {
+    for (DexProgramClass clazz : programClasses.collectLoadedClasses()) {
       for (DexEncodedMethod method : clazz.virtualMethods()) {
         if (options.methodMatchesFilter(method)) {
           disassemble(method, getProguardMap(), outputDir);
@@ -202,21 +224,48 @@
     if (clazz.type != dexItemFactory.objectType) {
       builder.append(".super ");
       builder.append(clazz.superType.toSmaliString());
-      // TODO(sgjesse): Add implemented interfaces
       builder.append("\n");
+      for (DexType iface : clazz.interfaces.values) {
+        builder.append(".implements ");
+        builder.append(iface.toSmaliString());
+        builder.append("\n");
+      }
     }
     ps.append(builder.toString());
   }
 
-  /** Write smali source for the application code on the provided PrintStream. */
+  private void writeClassFooter(DexClass clazz, PrintStream ps) {
+    StringBuilder builder = new StringBuilder();
+    builder.append("# End of class ");
+    builder.append(clazz.type.toSmaliString());
+    builder.append("\n");
+    ps.append(builder.toString());
+  }
+
+  /**
+   * Write smali source for the application code on the provided PrintStream.
+   */
   public void smali(InternalOptions options, PrintStream ps) {
-    List<DexProgramClass> classes = (List<DexProgramClass>) classes();
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
     classes.sort(Comparator.comparing(DexProgramClass::toSourceString));
+    boolean firstClass = true;
     for (DexClass clazz : classes) {
       boolean classHeaderWritten = false;
+      if (!options.hasMethodsFilter()) {
+        if (!firstClass) {
+          ps.append("\n");
+          firstClass = false;
+        }
+        writeClassHeader(clazz, ps);
+        classHeaderWritten = true;
+      }
       for (DexEncodedMethod method : clazz.virtualMethods()) {
         if (options.methodMatchesFilter(method)) {
           if (!classHeaderWritten) {
+            if (!firstClass) {
+              ps.append("\n");
+              firstClass = false;
+            }
             writeClassHeader(clazz, ps);
             classHeaderWritten = true;
           }
@@ -227,6 +276,10 @@
       for (DexEncodedMethod method : clazz.directMethods()) {
         if (options.methodMatchesFilter(method)) {
           if (!classHeaderWritten) {
+            if (!firstClass) {
+              ps.append("\n");
+              firstClass = false;
+            }
             writeClassHeader(clazz, ps);
             classHeaderWritten = true;
           }
@@ -234,13 +287,24 @@
           ps.append(method.toSmaliString(getProguardMap()));
         }
       }
+      if (classHeaderWritten) {
+        ps.append("\n");
+        writeClassFooter(clazz, ps);
+      }
     }
   }
 
   public static class Builder {
+    // We handle program class collection separately from classpath
+    // and library class collections. Since while we assume program
+    // class collection should always be fully loaded and thus fully
+    // represented by the map (making it easy, for example, adding
+    // new or removing existing classes), classpath and library
+    // collections will be considered monolithic collections.
 
-    private final Hashtable<DexType, DexClassPromise> classMap = new Hashtable<>();
-    private LazyClassCollection lazyClassCollection;
+    private final List<DexProgramClass> programClasses;
+    private ClasspathClassCollection classpathClasses;
+    private LibraryClassCollection libraryClasses;
 
     public final Hashtable<DexCode, DexCode> codeItems = new Hashtable<>();
 
@@ -252,18 +316,17 @@
     private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
 
     public Builder(DexItemFactory dexItemFactory, Timing timing) {
+      this.programClasses = new ArrayList<>();
       this.dexItemFactory = dexItemFactory;
       this.timing = timing;
-      this.lazyClassCollection = null;
+      this.classpathClasses = null;
+      this.libraryClasses = null;
     }
 
     public Builder(DexApplication application) {
-      this(application, application.classMap);
-    }
-
-    public Builder(DexApplication application, Map<DexType, DexClassPromise> classMap) {
-      this.classMap.putAll(classMap);
-      this.lazyClassCollection = application.lazyClassCollection;
+      programClasses = application.programClasses.collectLoadedClasses();
+      classpathClasses = application.classpathClasses;
+      libraryClasses = application.libraryClasses;
       proguardMap = application.proguardMap;
       timing = application.timing;
       highestSortingString = application.highestSortingString;
@@ -277,62 +340,45 @@
       return this;
     }
 
+    public synchronized Builder replaceProgramClasses(List<DexProgramClass> newProgramClasses) {
+      assert newProgramClasses != null;
+      this.programClasses.clear();
+      this.programClasses.addAll(newProgramClasses);
+      return this;
+    }
+
     public synchronized Builder setHighestSortingString(DexString value) {
       highestSortingString = value;
       return this;
     }
 
-    public Builder addClassPromise(DexClassPromise promise) {
-      addClassPromise(promise, false);
+    public synchronized Builder addProgramClass(DexProgramClass clazz) {
+      programClasses.add(clazz);
       return this;
     }
 
-    public Builder setLazyClassCollection(LazyClassCollection lazyClassMap) {
-      this.lazyClassCollection = lazyClassMap;
+    public Builder setClasspathClassCollection(ClasspathClassCollection classes) {
+      this.classpathClasses = classes;
       return this;
     }
 
-    public Builder addClassIgnoringLibraryDuplicates(DexClass clazz) {
-      addClass(clazz, true);
+    public Builder setLibraryClassCollection(LibraryClassCollection classes) {
+      this.libraryClasses = classes;
       return this;
     }
 
-    public Builder addSynthesizedClass(DexProgramClass synthesizedClass, boolean addToMainDexList) {
+    public synchronized Builder addSynthesizedClass(
+        DexProgramClass synthesizedClass, boolean addToMainDexList) {
       assert synthesizedClass.isProgramClass() : "All synthesized classes must be program classes";
-      addClassPromise(synthesizedClass);
+      addProgramClass(synthesizedClass);
       if (addToMainDexList && !mainDexList.isEmpty()) {
         mainDexList.add(synthesizedClass.type);
       }
       return this;
     }
 
-    public List<DexProgramClass> getProgramClasses() {
-      List<DexProgramClass> result = new ArrayList<>();
-      // Note: we ignore lazy class collection because it
-      // is never supposed to be used for program classes.
-      for (DexClassPromise promise : classMap.values()) {
-        if (promise.isProgramClass()) {
-          result.add(promise.get().asProgramClass());
-        }
-      }
-      return result;
-    }
-
-    // Callback from FileReader when parsing a DexProgramClass (multi-threaded).
-    private void addClass(DexClass clazz, boolean skipLibDups) {
-      addClassPromise(clazz, skipLibDups);
-    }
-
-    public synchronized void addClassPromise(DexClassPromise promise, boolean skipLibDups) {
-      assert promise != null;
-      DexType type = promise.getType();
-      DexClassPromise oldPromise = classMap.get(type);
-      if (oldPromise != null) {
-        promise = chooseClass(promise, oldPromise, skipLibDups);
-      }
-      if (oldPromise != promise) {
-        classMap.put(type, promise);
-      }
+    public Collection<DexProgramClass> getProgramClasses() {
+      return programClasses;
     }
 
     public Builder addToMainDexList(Collection<DexType> mainDexList) {
@@ -343,90 +389,13 @@
     public DexApplication build() {
       return new DexApplication(
           proguardMap,
-          ImmutableMap.copyOf(classMap),
-          lazyClassCollection,
+          ProgramClassCollection.create(programClasses),
+          classpathClasses,
+          libraryClasses,
           ImmutableSet.copyOf(mainDexList),
           dexItemFactory,
           highestSortingString,
           timing);
     }
   }
-
-  public static DexClassPromise chooseClass(
-      DexClassPromise a, DexClassPromise b, boolean skipLibDups) {
-    // NOTE: We assume that there should not be any conflicting names in user defined
-    // classes and/or linked jars. If we ever want to allow 'keep first'-like policy
-    // to resolve this kind of conflict between program and/or classpath classes, we'll
-    // need to make sure we choose the class we keep deterministically.
-    if (a.isProgramClass() && b.isProgramClass()) {
-      if (allowProgramClassConflict(a, b)) {
-        return a;
-      }
-      throw new CompilationError(
-          "Program type already present: " + a.getType().toSourceString());
-    }
-    if (a.isProgramClass()) {
-      return chooseBetweenProgramAndOtherClass(a, b);
-    }
-    if (b.isProgramClass()) {
-      return chooseBetweenProgramAndOtherClass(b, a);
-    }
-
-    if (a.isClasspathClass() && b.isClasspathClass()) {
-      throw new CompilationError(
-          "Classpath type already present: " + a.getType().toSourceString());
-    }
-    if (a.isClasspathClass()) {
-      return chooseBetweenClasspathAndLibraryClass(a, b);
-    }
-    if (b.isClasspathClass()) {
-      return chooseBetweenClasspathAndLibraryClass(b, a);
-    }
-
-    return chooseBetweenLibraryClasses(b, a, skipLibDups);
-  }
-
-  private static boolean allowProgramClassConflict(DexClassPromise a, DexClassPromise b) {
-    // Currently only allow collapsing synthetic lambda classes.
-    return a.getOrigin() == Resource.Kind.DEX
-        && b.getOrigin() == Resource.Kind.DEX
-        && a.get().accessFlags.isSynthetic()
-        && b.get().accessFlags.isSynthetic()
-        && LambdaRewriter.hasLambdaClassPrefix(a.getType())
-        && LambdaRewriter.hasLambdaClassPrefix(b.getType());
-  }
-
-  private static DexClassPromise chooseBetweenProgramAndOtherClass(
-      DexClassPromise selected, DexClassPromise ignored) {
-    assert selected.isProgramClass() && !ignored.isProgramClass();
-    if (ignored.isLibraryClass()) {
-      logIgnoredClass(ignored, "Class `%s` was specified as library and program type.");
-    }
-    // We don't log program/classpath class conflict since it is expected case.
-    return selected;
-  }
-
-  private static DexClassPromise chooseBetweenClasspathAndLibraryClass(
-      DexClassPromise selected, DexClassPromise ignored) {
-    assert selected.isClasspathClass() && ignored.isLibraryClass();
-    logIgnoredClass(ignored, "Class `%s` was specified as library and classpath type.");
-    return selected;
-  }
-
-  private static DexClassPromise chooseBetweenLibraryClasses(
-      DexClassPromise selected, DexClassPromise ignored, boolean skipDups) {
-    assert selected.isLibraryClass() && ignored.isLibraryClass();
-    if (!skipDups) {
-      throw new CompilationError(
-          "Library type already present: " + selected.getType().toSourceString());
-    }
-    logIgnoredClass(ignored, "Class `%s` was specified twice as a library type.");
-    return selected;
-  }
-
-  private static void logIgnoredClass(DexClassPromise ignored, String message) {
-    if (Log.ENABLED) {
-      Log.warn(DexApplication.class, message, ignored.getType().toSourceString());
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index f684309..8560de3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.google.common.base.MoreObjects;
 
-public abstract class DexClass extends DexItem implements DexClassPromise {
+public abstract class DexClass extends DexItem {
 
   private static final DexEncodedMethod[] NO_METHODS = {};
   private static final DexEncodedField[] NO_FIELDS = {};
@@ -50,6 +50,12 @@
         throw new CompilationError("Interface " + type.toString() + " cannot implement itself");
       }
     }
+    if (!type.descriptor.isValidClassDescriptor()) {
+      throw new CompilationError(
+          "Class descriptor '"
+              + type.descriptor.toString()
+              + "' cannot be represented in dex format.");
+    }
   }
 
   @Override
@@ -118,7 +124,6 @@
 
   public abstract void addDependencies(MixedSectionCollection collector);
 
-  @Override
   public boolean isProgramClass() {
     return false;
   }
@@ -127,7 +132,6 @@
     return null;
   }
 
-  @Override
   public boolean isClasspathClass() {
     return false;
   }
@@ -136,7 +140,6 @@
     return null;
   }
 
-  @Override
   public boolean isLibraryClass() {
     return false;
   }
@@ -154,21 +157,10 @@
     return null;
   }
 
-  @Override
   public Resource.Kind getOrigin() {
     return this.origin;
   }
 
-  @Override
-  public DexClass get() {
-    return this;
-  }
-
-  @Override
-  public DexType getType() {
-    return type;
-  }
-
   public boolean hasClassInitializer() {
     return getClassInitializer() != null;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassPromise.java b/src/main/java/com/android/tools/r8/graph/DexClassPromise.java
deleted file mode 100644
index c0bdf20..0000000
--- a/src/main/java/com/android/tools/r8/graph/DexClassPromise.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.graph;
-
-import com.android.tools.r8.Resource;
-
-/**
- * Provides a way for delayed DexClass discovery.
- *
- * Provides minimal class details of the promised class and
- * provides the class when asked by calling method get().
- *
- * Note that DexClass also implements this interface, since it
- * represents a 'materialized' promise for a class.
- */
-public interface DexClassPromise {
-  DexType getType();
-
-  Resource.Kind getOrigin();
-
-  boolean isProgramClass();
-
-  boolean isClasspathClass();
-
-  boolean isLibraryClass();
-
-  DexClass get();
-}
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index ebfd388..f48e7db 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -15,9 +15,11 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
@@ -157,10 +159,10 @@
   }
 
   public String toString() {
-    return toString(null);
+    return toString(null, null);
   }
 
-  public String toString(ClassNameMapper naming) {
+  public String toString(DexEncodedMethod method, ClassNameMapper naming) {
     StringBuilder builder = new StringBuilder();
     builder.append("registers: ").append(registerSize);
     builder.append(", inputs: ").append(incomingRegisterSize);
@@ -168,12 +170,22 @@
     builder.append("------------------------------------------------------------\n");
     builder.append("inst#  offset  instruction         arguments\n");
     builder.append("------------------------------------------------------------\n");
+    DexDebugEntry debugInfo = null;
+    Iterator<DexDebugEntry> debugInfoIterator = Collections.emptyIterator();
+    if (getDebugInfo() != null && method != null) {
+      debugInfoIterator = new DexDebugEntryBuilder(method, new DexItemFactory()).build().iterator();
+      debugInfo = debugInfoIterator.hasNext() ? debugInfoIterator.next() : null;
+    }
     int instructionNumber = 0;
     for (Instruction insn : instructions) {
       StringUtils.appendLeftPadded(builder, Integer.toString(instructionNumber++), 5);
       builder.append(": ")
           .append(insn.toString(naming))
           .append('\n');
+      if (debugInfo != null && debugInfo.address == insn.getOffset()) {
+        builder.append("      ").append(debugInfo).append("\n");
+        debugInfo = debugInfoIterator.hasNext() ? debugInfoIterator.next() : null;
+      }
     }
     if (tries.length > 0) {
       builder.append("Tries (numbers are offsets)\n");
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
index f6d8fb4..4410f2a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEntryBuilder.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.ir.code.MoveType;
 import com.google.common.collect.ImmutableMap;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -60,19 +61,19 @@
     DexCode code = method.getCode().asDexCode();
     DexDebugInfo info = code.getDebugInfo();
     int argumentRegister = code.registerSize - code.incomingRegisterSize;
-    int argumentCount = code.incomingRegisterSize;
     if (!method.accessFlags.isStatic()) {
-      --argumentCount;
       DexString name = factory.thisName;
       DexType type = method.method.getHolder();
-      startLocal(argumentRegister++, name, type, null);
+      startLocal(argumentRegister, name, type, null);
+      argumentRegister += MoveType.fromDexType(type).requiredRegisters();
     }
     DexType[] types = method.method.proto.parameters.values;
     DexString[] names = info.parameters;
-    for (int i = 0; i < argumentCount; i++) {
+    for (int i = 0; i < types.length; i++) {
       // If null, the parameter has a parameterized type and the local is introduced in the stream.
       if (names[i] != null) {
-        startLocal(argumentRegister++, names[i], types[i], null);
+        startLocal(argumentRegister, names[i], types[i], null);
+        argumentRegister += MoveType.fromDexType(types[i]).requiredRegisters();
       }
     }
     currentLine = info.startLine;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 188440d..3df2fab 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -22,7 +22,7 @@
 import com.android.tools.r8.ir.code.MoveType;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
@@ -73,6 +73,11 @@
     return compilationState != CompilationState.NOT_PROCESSED;
   }
 
+  public boolean cannotInline() {
+    return compilationState == CompilationState.NOT_PROCESSED
+        || compilationState == CompilationState.PROCESSED_NOT_INLINING_CANDIDATE;
+  }
+
   public boolean isInliningCandidate(DexEncodedMethod container, boolean alwaysInline) {
     if (container.accessFlags.isStatic() && container.accessFlags.isConstructor()) {
       // This will probably never happen but never inline a class initializer.
@@ -102,7 +107,8 @@
     return compilationState == PROCESSED_INLINING_CANDIDATE_PUBLIC;
   }
 
-  public void markProcessed(InliningConstraint state) {
+  public boolean markProcessed(Constraint state) {
+    CompilationState prevCompilationState = compilationState;
     switch (state) {
       case ALWAYS:
         compilationState = PROCESSED_INLINING_CANDIDATE_PUBLIC;
@@ -117,6 +123,7 @@
         compilationState = PROCESSED_NOT_INLINING_CANDIDATE;
         break;
     }
+    return prevCompilationState != compilationState;
   }
 
   public void markNotProcessed() {
@@ -341,7 +348,7 @@
   }
 
   public String codeToString() {
-    return code == null ? "<no code>" : code.toString();
+    return code == null ? "<no code>" : code.toString(this, null);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index f97c147..5616967 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.dex.IndexedItemCollection;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.naming.NamingLens;
 
 public class DexField extends Descriptor<DexEncodedField, DexField> implements
@@ -17,6 +18,10 @@
     this.clazz = clazz;
     this.type = type;
     this.name = name;
+    if (!name.isValidFieldName()) {
+      throw new CompilationError(
+          "Field name '" + name.toString() + "' cannot be represented in dex format.");
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index de536f6..a066d24 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.dex.IndexedItemCollection;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.naming.NamingLens;
 
 public class DexMethod extends Descriptor<DexEncodedMethod, DexMethod>
@@ -20,6 +21,10 @@
     this.holder = holder;
     this.proto = proto;
     this.name = name;
+    if (!name.isValidMethodName()) {
+      throw new CompilationError(
+          "Method name '" + name.toString() + "' cannot be represented in dex format.");
+    }
   }
 
   @Override
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 e9b687e..e70e8f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -185,6 +185,127 @@
     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'
+        || string.charAt(string.length() - 1) != ';') {
+      return false;
+    }
+    if (string.charAt(1) == '/' || string.charAt(string.length() - 2) == '/') {
+      return false;
+    }
+    for (int i = 1; i < string.length() - 1; i++) {
+      char ch = string.charAt(i);
+      if (ch == '/') {
+        continue;
+      }
+      if (isSimpleNameChar(ch)) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  private boolean isValidMethodName(String string) {
+    if (string.isEmpty()) {
+      return false;
+    }
+    // According to https://source.android.com/devices/tech/dalvik/dex-format#membername
+    // '<' SimpleName '>' should be valid. However, the art verifier only allows <init>
+    // and <clinit> which is reasonable.
+    if ((string.charAt(0) == '<') &&
+        (string.equals("<init>") || string.equals("<clinit>"))) {
+      return true;
+    }
+    for (int i = 0; i < string.length(); i++) {
+      char ch = string.charAt(i);
+      if (isSimpleNameChar(ch)) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  private boolean isValidFieldName(String string) {
+    if (string.isEmpty()) {
+      return false;
+    }
+    int start = 0;
+    int end = string.length();
+    if (string.charAt(0) == '<') {
+      if (string.charAt(string.length() - 1) == '>') {
+        start = 1;
+        end = string.length() - 1;
+      } else {
+        return false;
+      }
+    }
+    for (int i = start; i < end; i++) {
+      if (isSimpleNameChar(string.charAt(i))) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  public boolean isValidMethodName() {
+    try {
+      return isValidMethodName(decode());
+    } catch (UTFDataFormatException e) {
+      return false;
+    }
+  }
+
+  public boolean isValidFieldName() {
+    try {
+      return isValidFieldName(decode());
+    } catch (UTFDataFormatException e) {
+      return false;
+    }
+  }
+
+  public boolean isValidClassDescriptor() {
+    try {
+      return isValidClassDescriptor(decode());
+    } catch (UTFDataFormatException e) {
+      return false;
+    }
+  }
+
   public String dump() {
     StringBuilder builder = new StringBuilder();
     builder.append(toString());
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index e6c2d00..b9173dd 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -6,6 +6,19 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 
+/**
+ * A GraphLense implements a virtual view on top of the graph, used to delay global rewrites until
+ * later IR processing stages.
+ * <p>
+ * Valid remappings are limited to the following operations:
+ * <ul>
+ * <li>Mapping a classes type to one of the super/subtypes.</li>
+ * <li>Renaming private methods/fields.</li>
+ * <li>Moving methods/fields to a super/subclass.</li>
+ * <li>Replacing method/field references by the same method/field on a super/subtype</li>
+ * </ul>
+ * Note that the latter two have to take visibility into account.
+ */
 public abstract class GraphLense {
 
   public static class Builder {
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 2766684..9a367ff 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -112,7 +112,7 @@
   }
 
   @Override
-  public String toString(ClassNameMapper naming) {
+  public String toString(DexEncodedMethod method, ClassNameMapper naming) {
     return toString();
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java b/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
deleted file mode 100644
index e5792cd..0000000
--- a/src/main/java/com/android/tools/r8/graph/LazyClassFileLoader.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.graph;
-
-import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
-import com.google.common.io.Closer;
-import java.io.IOException;
-
-// Lazily loads a class file represented by resource.
-public final class LazyClassFileLoader implements DexClassPromise {
-  // Resource representing file definition.
-  private final Resource resource;
-  // Class kind to be created.
-  private final ClassKind classKind;
-
-  // Application reader to be used. Note that the reader may be reused in
-  // many loaders and may be used concurrently, it is considered to be
-  // thread-safe since its only state is internal options which we
-  // consider immutable after they are initialized (barring dex factory
-  // which is thread-safe).
-  private final JarApplicationReader reader;
-
-  // Dex type of the class to be created.
-  private final DexType type;
-
-  // Cached loaded class if get(...) method has already been called, this
-  // field is only accessed in context synchronized on `this`.
-  private DexClass loadedClass = null;
-
-  public LazyClassFileLoader(DexType type,
-      Resource resource, ClassKind classKind, JarApplicationReader reader) {
-    this.resource = resource;
-    this.reader = reader;
-    this.type = type;
-    this.classKind = classKind;
-    assert classKind != ClassKind.PROGRAM;
-  }
-
-  // Callback method for JarClassFileReader, is always called in synchronized context.
-  private void addClass(DexClass clazz) {
-    assert clazz != null;
-    assert loadedClass == null;
-    loadedClass = clazz;
-  }
-
-  @Override
-  public DexType getType() {
-    return type;
-  }
-
-  @Override
-  public Resource.Kind getOrigin() {
-    return Resource.Kind.CLASSFILE;
-  }
-
-  @Override
-  public boolean isProgramClass() {
-    return false;
-  }
-
-  @Override
-  public boolean isClasspathClass() {
-    return classKind == ClassKind.CLASSPATH;
-  }
-
-  @Override
-  public boolean isLibraryClass() {
-    return classKind == ClassKind.LIBRARY;
-  }
-
-  // Loads the class from the resource. Synchronized on `this` to avoid
-  // unnecessary complications, thus all threads trying to load a class with
-  // this loader will wait for the first load to finish.
-  @Override
-  public synchronized DexClass get() {
-    if (loadedClass != null) {
-      return loadedClass;
-    }
-
-    try (Closer closer = Closer.create()) {
-      JarClassFileReader reader = new JarClassFileReader(this.reader, this::addClass);
-      reader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
-    } catch (IOException e) {
-      throw new CompilationError("Failed to load class: " + type.toSourceString(), e);
-    }
-
-    if (loadedClass == null) {
-      throw new Unreachable("Class is supposed to be loaded: " + type.toSourceString());
-    }
-
-    if (loadedClass.type != type) {
-      throw new CompilationError("Class content provided for type descriptor "
-          + type.toSourceString() + " actually defines class " + loadedClass.type.toSourceString());
-    }
-
-    return loadedClass;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
index 1d5f38c..7dab70a 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
@@ -79,6 +79,7 @@
 
   private static DexProgramClass[] sortClasses(
       DexApplication application, DexProgramClass[] classes) {
+    Arrays.sort(classes, (o1, o2) -> o1.type.descriptor.slowCompareTo(o2.type.descriptor));
     SortingProgramClassVisitor classVisitor = new SortingProgramClassVisitor(application, classes);
     classVisitor.run(classes);
     return classVisitor.getSortedClasses();
diff --git a/src/main/java/com/android/tools/r8/ir/code/Argument.java b/src/main/java/com/android/tools/r8/ir/code/Argument.java
index 94c415e..94f6507 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Argument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Argument.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
 
 /**
@@ -67,7 +67,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 8bb31d2..ab2056d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -119,7 +120,7 @@
   public List<BasicBlock> getNormalPredecessors() {
     ImmutableList.Builder<BasicBlock> normals = ImmutableList.builder();
     for (BasicBlock predecessor : predecessors) {
-      if (!predecessor.isCatchSuccessor(this)) {
+      if (!predecessor.hasCatchSuccessor(this)) {
         normals.add(predecessor);
       }
     }
@@ -647,10 +648,10 @@
 
   public EdgeType getEdgeType(BasicBlock successor) {
     assert successors.indexOf(successor) >= 0;
-    return isCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
+    return hasCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
   }
 
-  public boolean isCatchSuccessor(BasicBlock block) {
+  public boolean hasCatchSuccessor(BasicBlock block) {
     if (!hasCatchHandlers()) {
       return false;
     }
@@ -658,7 +659,7 @@
   }
 
   public int guardsForCatchSuccessor(BasicBlock block) {
-    assert isCatchSuccessor(block);
+    assert hasCatchSuccessor(block);
     int index = successors.indexOf(block);
     int count = 0;
     for (int handler : catchHandlers.getAllTargets()) {
@@ -694,7 +695,7 @@
       StringBuilder builder, List<BasicBlock> list, Function<BasicBlock, String> postfix) {
     if (list.size() > 0) {
       for (BasicBlock block : list) {
-        builder.append(block.getNumber());
+        builder.append(block.number >= 0 ? block.number : "<unknown>");
         builder.append(postfix.apply(block));
         builder.append(" ");
       }
@@ -713,7 +714,7 @@
   }
 
   private String predecessorPostfix(BasicBlock block) {
-    if (isCatchSuccessor(block)) {
+    if (hasCatchSuccessor(block)) {
       return new String(new char[guardsForCatchSuccessor(block)]).replace("\0", "*");
     }
     return "";
@@ -1053,7 +1054,7 @@
     // one new phi to merge the two exception values, and all other phis don't need
     // to be changed.
     for (BasicBlock catchSuccessor : catchSuccessors) {
-      catchSuccessor.splitCriticalExceptioEdges(
+      catchSuccessor.splitCriticalExceptionEdges(
           code.valueNumberGenerator,
           newBlock -> {
             newBlock.setNumber(code.blocks.size());
@@ -1062,15 +1063,6 @@
     }
   }
 
-  private boolean allPredecessorsHaveCatchEdges() {
-    for (BasicBlock predecessor : getPredecessors()) {
-      if (!predecessor.isCatchSuccessor(this)) {
-        assert false;
-      }
-    }
-    return true;
-  }
-
   /**
    * Assumes that `this` block is a catch handler target (note that it does not have to
    * start with MoveException instruction, since the instruction can be removed by
@@ -1086,10 +1078,8 @@
    *
    * NOTE: onNewBlock must assign block number to the newly created block.
    */
-  public void splitCriticalExceptioEdges(
+  public void splitCriticalExceptionEdges(
       ValueNumberGenerator valueNumberGenerator, Consumer<BasicBlock> onNewBlock) {
-    assert allPredecessorsHaveCatchEdges();
-
     List<BasicBlock> predecessors = this.getPredecessors();
     boolean hasMoveException = entry().isMoveException();
     MoveException move = null;
@@ -1103,6 +1093,10 @@
     List<BasicBlock> newPredecessors = new ArrayList<>();
     List<Value> values = new ArrayList<>(predecessors.size());
     for (BasicBlock predecessor : predecessors) {
+      if (!predecessor.hasCatchSuccessor(this)) {
+        throw new CompilationError(
+            "Invalid block structure: catch block reachable via non-exceptional flow.");
+      }
       BasicBlock newBlock = new BasicBlock();
       newPredecessors.add(newBlock);
       if (hasMoveException) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Binop.java b/src/main/java/com/android/tools/r8/ir/code/Binop.java
index 83240bb..899516a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Binop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Binop.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public abstract class Binop extends Instruction {
 
@@ -112,7 +112,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 451832f..4cca9dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -15,7 +15,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.NumberUtils;
 
 public class ConstNumber extends ConstInstruction {
@@ -188,7 +188,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Div.java b/src/main/java/com/android/tools/r8/ir/code/Div.java
index c8c1c32..8fd9bf0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Div.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Div.java
@@ -14,9 +14,6 @@
 import com.android.tools.r8.code.DivIntLit8;
 import com.android.tools.r8.code.DivLong;
 import com.android.tools.r8.code.DivLong2Addr;
-import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
 
 public class Div extends ArithmeticBinop {
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index ba1e0e9..0b8d876 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -9,8 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import java.util.List;
 
 abstract class FieldInstruction extends Instruction {
@@ -43,7 +42,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
     // Resolve the field if possible and decide whether the instruction can inlined.
     DexType fieldHolder = field.getHolder();
     DexEncodedField target = info.lookupInstanceTarget(fieldHolder, field);
@@ -51,15 +50,15 @@
     if ((target != null) && (fieldClass != null) && !fieldClass.isLibraryClass()) {
       DexAccessFlags flags = target.accessFlags;
       if (flags.isPublic()) {
-        return Inliner.InliningConstraint.ALWAYS;
+        return Constraint.ALWAYS;
       }
       if (flags.isPrivate() && (fieldHolder == holder)) {
-        return Inliner.InliningConstraint.PRIVATE;
+        return Constraint.PRIVATE;
       }
       if (flags.isProtected() && (fieldHolder.isSamePackage(holder))) {
-        return Inliner.InliningConstraint.PACKAGE;
+        return Constraint.PACKAGE;
       }
     }
-    return Inliner.InliningConstraint.NEVER;
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
index 3fe498d..1126299 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class InstanceOf extends Instruction {
 
@@ -75,14 +75,14 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
     DexClass targetClass = info.definitionFor(type());
     if (targetClass == null) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
     if (targetClass.accessFlags.isPublic()) {
-      return InliningConstraint.ALWAYS;
+      return Constraint.ALWAYS;
     }
-    return InliningConstraint.NEVER;
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 1b18ccc..2e821cf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.Value.DebugInfo;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
@@ -797,7 +797,7 @@
   }
 
   // Returns the inlining constraint for this instruction.
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.NEVER;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 5b516b7..14104d7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -91,8 +91,14 @@
   }
 
   protected int argumentRegisterValue(int i, DexBuilder builder) {
+    assert needsRangedInvoke(builder);
     if (i < arguments().size()) {
-      return builder.allocatedRegister(arguments().get(i), getNumber());
+      // If argument values flow into ranged invokes, all the ranged invoke arguments
+      // are arguments to this method in order. Therefore, we use the incoming registers
+      // for the ranged invoke arguments. We know that arguments are always available there.
+      // If argument reuse is allowed there is no splitting and if argument reuse is disallowed
+      // the argument registers are never overwritten.
+      return builder.argumentOrAllocateRegister(arguments().get(i), getNumber());
     }
     return 0;
   }
@@ -120,10 +126,10 @@
 
   protected boolean argumentsConsecutive(DexBuilder builder) {
     Value value = arguments().get(0);
-    int next = builder.allocatedRegister(value, getNumber()) + value.requiredRegisters();
+    int next = builder.argumentOrAllocateRegister(value, getNumber()) + value.requiredRegisters();
     for (int i = 1; i < arguments().size(); i++) {
       value = arguments().get(i);
-      assert next == builder.allocatedRegister(value, getNumber());
+      assert next == builder.argumentOrAllocateRegister(value, getNumber());
       next += value.requiredRegisters();
     }
     return true;
@@ -162,9 +168,45 @@
     if (requiredArgumentRegisters() > 5) {
       return Constants.U16BIT_MAX;
     }
+    if (argumentsAreConsecutiveInputArguments()) {
+      return Constants.U16BIT_MAX;
+    }
     return Constants.U4BIT_MAX;
   }
 
+  private boolean argumentsAreConsecutiveInputArguments() {
+    if (arguments().size() == 0) {
+      return false;
+    }
+    Value current = arguments().get(0);
+    if (!current.isArgument()) {
+      return false;
+    }
+    for (int i = 1; i < arguments().size(); i++) {
+      Value next = arguments().get(i);
+      if (current.getNextConsecutive() != next) {
+        return false;
+      }
+      current = next;
+    }
+    return true;
+  }
+
+  private boolean argumentsAreConsecutiveInputArgumentsWithHighRegisters(
+      DexBuilder builder) {
+    if (!argumentsAreConsecutiveInputArguments()) {
+      return false;
+    }
+    Value lastArgument = arguments().get(arguments().size() - 1);
+    return builder.argumentOrAllocateRegister(lastArgument, getNumber()) > Constants.U4BIT_MAX;
+  }
+
+  protected boolean needsRangedInvoke(DexBuilder builder) {
+    return requiredArgumentRegisters() > 5
+        || hasHighArgumentRegister(builder)
+        || argumentsAreConsecutiveInputArgumentsWithHighRegisters(builder);
+  }
+
   @Override
   public int maxOutValueRegister() {
     return Constants.U8BIT_MAX;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
index 2c7ea4b..d0d0739 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeCustom.java
@@ -50,7 +50,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeCustomRange(firstRegister, argumentRegisters, getCallSite());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 95776f4..f11d62f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -39,7 +39,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeDirectRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
index f1c1d65..9d0c01f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeInterface.java
@@ -38,7 +38,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeInterfaceRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
index 0725fed..2912c1a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeNewArray.java
@@ -49,7 +49,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new FilledNewArrayRange(firstRegister, argumentRegisters, type);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index 9c87144..efc12db 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -47,7 +47,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokePolymorphicRange(
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 0c4fe14..dc22e2e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -38,7 +38,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeStaticRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
index 2e1e3e7..fc79c11 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeSuper.java
@@ -38,7 +38,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeSuperRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index 339ae71..3988980 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -38,7 +38,7 @@
     com.android.tools.r8.code.Instruction instruction;
     int argumentRegisters = requiredArgumentRegisters();
     builder.requestOutgoingRegisters(argumentRegisters);
-    if (argumentRegisters > 5 || hasHighArgumentRegister(builder)) {
+    if (needsRangedInvoke(builder)) {
       assert argumentsConsecutive(builder);
       int firstRegister = argumentRegisterValue(0, builder);
       instruction = new InvokeVirtualRange(firstRegister, argumentRegisters, getInvokedMethod());
diff --git a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
index 2a02218..7b1c007 100644
--- a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
@@ -5,7 +5,7 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
 
@@ -47,7 +47,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 869d1d0..b3e155b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -29,7 +29,15 @@
 
   @Override
   public void buildDex(DexBuilder builder) {
-    int object = builder.allocatedRegister(object(), getNumber());
+    // If the monitor object is an argument, we use the argument register for all the monitor
+    // enters and exits in order to not confuse the Art verifier lock verification code.
+    // This is best effort. If the argument happens to be in a very high register we cannot
+    // do it and the lock verification can hit a case where it gets confused. Not much we
+    // can do about that, but this should avoid it in the most common cases.
+    int object = builder.argumentOrAllocateRegister(object(), getNumber());
+    if (object > maxInValueRegister()) {
+      object = builder.allocatedRegister(object(), getNumber());
+    }
     if (type == Type.ENTER) {
       builder.add(this, new MonitorEnter(object));
     } else {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 6f70c57..91e19f0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Move extends Instruction {
 
@@ -81,7 +81,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 5912f0f..de44e83 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.BasicBlock.EdgeType;
 import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -60,6 +61,9 @@
     // exactly once by adding the operands.
     assert operands.isEmpty();
     boolean canBeNull = false;
+    if (block.getPredecessors().size() == 0) {
+      throwUndefinedValueError();
+    }
     for (BasicBlock pred : block.getPredecessors()) {
       EdgeType edgeType = pred.getEdgeType(block);
       // Since this read has been delayed we must provide the local info for the value.
@@ -79,6 +83,9 @@
     // exactly once by adding the operands.
     assert this.operands.isEmpty();
     boolean canBeNull = false;
+    if (operands.size() == 0) {
+      throwUndefinedValueError();
+    }
     for (Value operand : operands) {
       canBeNull |= operand.canBeNull();
       appendOperand(operand);
@@ -89,6 +96,13 @@
     removeTrivialPhi();
   }
 
+  private void throwUndefinedValueError() {
+    throw new CompilationError(
+        "Undefined value encountered during compilation. "
+            + "This is typically caused by invalid dex input that uses a register "
+            + "that is not define on all control-flow paths leading to the use.");
+  }
+
   private void appendOperand(Value operand) {
     operands.add(operand);
     operand.addPhiUser(this);
@@ -174,9 +188,6 @@
       same = op;
     }
     assert isTrivialPhi();
-    if (same == null) {
-      same = Value.UNDEFINED;
-    }
     // Removing this phi, so get rid of it as a phi user from all of the operands to avoid
     // recursively getting back here with the same phi. If the phi has itself as an operand
     // that also removes the self-reference.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 2819c58..142206c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Return extends JumpInstruction {
 
@@ -110,7 +110,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Throw.java b/src/main/java/com/android/tools/r8/ir/code/Throw.java
index e0ba866..9bd6a81 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Throw.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Throw.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Throw extends JumpInstruction {
 
@@ -63,7 +63,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Unop.java b/src/main/java/com/android/tools/r8/ir/code/Unop.java
index ff1744a..174b7b6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Unop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Unop.java
@@ -6,7 +6,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 abstract public class Unop extends Instruction {
 
@@ -43,7 +43,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index bd48134..c506867 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -19,6 +19,8 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -134,10 +136,14 @@
 
     private final List<DexEncodedMethod> leaves;
     private final boolean brokeCycles;
+    private final Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls;
 
-    private Leaves(List<DexEncodedMethod> leaves, boolean brokeCycles) {
+    private Leaves(List<DexEncodedMethod> leaves, boolean brokeCycles,
+        Map<DexEncodedMethod, Set<DexEncodedMethod>> cycleBreakingCalls) {
       this.leaves = leaves;
       this.brokeCycles = brokeCycles;
+      this.cycleBreakingCalls = cycleBreakingCalls;
+      assert brokeCycles == (cycleBreakingCalls.size() != 0);
     }
 
     public int size() {
@@ -152,31 +158,38 @@
       return brokeCycles;
     }
 
+    /**
+     * Calls that were broken to produce the leaves.
+     *
+     * If {@link Leaves#breakCycles()} return <code>true</code> this provides the calls for each
+     * leaf that were broken.
+     *
+     * NOTE: The broken calls are not confined to the set of leaves.
+     */
+    public Map<DexEncodedMethod, Set<DexEncodedMethod>> getCycleBreakingCalls() {
+      return cycleBreakingCalls;
+    }
+
     @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
       builder.append("Leaves: ");
       builder.append(leaves.size());
       builder.append("\n");
-      builder.append(brokeCycles ? "Cycles broken" : "No cycles broken");
+      builder.append(brokeCycles ? "Call cycles broken" : "No call cycles broken");
       return builder.toString();
     }
   }
 
-  private final GraphLense graphLense;
   private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
   private List<Node> leaves = null;
   private Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
   private Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
 
-  private CallGraph(GraphLense graphLense) {
-    this.graphLense = graphLense;
-  }
-
   public static CallGraph build(DexApplication application, AppInfoWithSubtyping appInfo,
       GraphLense graphLense) {
 
-    CallGraph graph = new CallGraph(graphLense);
+    CallGraph graph = new CallGraph();
 
     for (DexClass clazz : application.classes()) {
       for (DexEncodedMethod method : clazz.directMethods()) {
@@ -279,18 +292,19 @@
    * <p>
    *
    * @return object with the leaves as a List of {@link DexEncodedMethod} and <code>boolean</code>
-   * indication of whether cycels where broken to produce leaves. <code>null</code> if the graph is
+   * indication of whether cycles were broken to produce leaves. <code>null</code> if the graph is
    * empty.
    */
   public Leaves pickLeaves() {
     boolean cyclesBroken = false;
+    Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = Collections.emptyMap();
     if (isEmpty()) {
       return null;
     }
     List<DexEncodedMethod> leaves = removeLeaves();
     if (leaves.size() == 0) {
       // The graph had no more leaves, so break cycles to construct leaves.
-      breakCycles();
+      brokenCalls = breakCycles();
       cyclesBroken = true;
       leaves = removeLeaves();
     }
@@ -298,7 +312,7 @@
     for (DexEncodedMethod leaf : leaves) {
       assert !leaf.isProcessed();
     }
-    return new Leaves(leaves, cyclesBroken);
+    return new Leaves(leaves, cyclesBroken, brokenCalls);
   }
 
   /**
@@ -309,35 +323,41 @@
    * (outgoing) degree.
    * <p>
    * It will avoid removing edges from bridge-methods if possible.
+   * <p>
+   * Returns the calls that were broken.
    */
-  private void breakCycles() {
+  private Map<DexEncodedMethod, Set<DexEncodedMethod>> breakCycles() {
+    Map<DexEncodedMethod, Set<DexEncodedMethod>> brokenCalls = new IdentityHashMap<>();
     // Break non bridges with degree 1.
     int minDegree = nodes.size();
     for (Node node : nodes.values()) {
       // Break cycles and add all leaves created in the process.
       if (!node.isBridge() && node.callDegree() <= 1) {
         assert node.callDegree() == 1;
-        removeAllCalls(node);
+        Set<DexEncodedMethod> calls = removeAllCalls(node);
         leaves.add(node);
+        brokenCalls.put(node.method, calls);
       } else {
         minDegree = Integer.min(minDegree, node.callDegree());
       }
     }
 
-    // Return if new leaves where created.
+    // Return if new leaves were created.
     if (leaves.size() > 0) {
-      return;
+      return brokenCalls;
     }
 
     // Break methods with the found minimum degree and add all leaves created in the process.
     for (Node node : nodes.values()) {
       if (node.callDegree() <= minDegree) {
         assert node.callDegree() == minDegree;
-        removeAllCalls(node);
+        Set<DexEncodedMethod> calls = removeAllCalls(node);
         leaves.add(node);
+        brokenCalls.put(node.method, calls);
       }
     }
     assert leaves.size() > 0;
+    return brokenCalls;
   }
 
   synchronized private Node ensureMethodNode(DexEncodedMethod method) {
@@ -356,11 +376,14 @@
     callee.invokeCount++;
   }
 
-  private void removeAllCalls(Node node) {
+  private Set<DexEncodedMethod> removeAllCalls(Node node) {
+    Set<DexEncodedMethod> calls = Sets.newIdentityHashSet();
     for (Node call : node.calls) {
+      calls.add(call.method);
       call.callees.remove(node);
     }
     node.calls.clear();
+    return calls;
   }
 
   private void remove(Node node, List<Node> leaves) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 4484aec..50afe5f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -318,6 +318,12 @@
     return registerAllocator.getRegisterForValue(value, instructionNumber);
   }
 
+  // Get the argument register for a value if it is an argument, otherwise returns the
+  // allocated register at the instruction number.
+  public int argumentOrAllocateRegister(Value value, int instructionNumber) {
+    return registerAllocator.getArgumentOrAllocateRegisterForValue(value, instructionNumber);
+  }
+
   public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
     return registerAllocator.argumentValueUsesHighRegister(value, instructionNumber);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 7e47ed2..118d0eb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -268,7 +268,7 @@
   }
 
   @Override
-  public boolean traceInstruction(int index, IRBuilder builder) {
+  public int traceInstruction(int index, IRBuilder builder) {
     Instruction dex = code.instructions[index];
     int offset = dex.getOffset();
     assert !dex.isPayload();
@@ -277,9 +277,9 @@
       // Check that we don't ever have instructions that can throw and have targets.
       assert !dex.canThrow();
       for (int relativeOffset : targets) {
-        builder.ensureSuccessorBlock(offset + relativeOffset);
+        builder.ensureNormalSuccessorBlock(offset, offset + relativeOffset);
       }
-      return true;
+      return index;
     }
     if (dex.canThrow()) {
       // If the instruction can throw and is in a try block, add edges to its catch successors.
@@ -297,7 +297,7 @@
         builder.ensureBlockWithoutEnqueuing(tryRangeStartAddress);
         // Edge to exceptional successors.
         for (Integer handlerOffset : getUniqueTryHandlerOffsets(tryRange)) {
-          builder.ensureSuccessorBlock(handlerOffset);
+          builder.ensureExceptionalSuccessorBlock(offset, handlerOffset);
         }
         // If the following instruction is a move-result include it in this (the invokes) block.
         if (index + 1 < code.instructions.length && isMoveResult(code.instructions[index + 1])) {
@@ -307,29 +307,29 @@
         }
         // Edge to normal successor if any (fallthrough).
         if (!(dex instanceof Throw)) {
-          builder.ensureSuccessorBlock(dex.getOffset() + dex.getSize());
+          builder.ensureNormalSuccessorBlock(offset, dex.getOffset() + dex.getSize());
         }
-        return true;
+        return index;
       }
       // Close the block if the instruction is a throw, otherwise the block remains open.
-      return dex instanceof Throw;
+      return dex instanceof Throw ? index : -1;
     }
     if (dex.isSwitch()) {
       // TODO(zerny): Remove this from block computation.
       switchPayloadResolver.addPayloadUser(dex);
 
       for (int target : switchPayloadResolver.absoluteTargets(dex)) {
-        builder.ensureSuccessorBlock(target);
+        builder.ensureNormalSuccessorBlock(offset, target);
       }
-      builder.ensureSuccessorBlock(offset + dex.getSize());
-      return true;
+      builder.ensureNormalSuccessorBlock(offset, offset + dex.getSize());
+      return index;
     }
     // TODO(zerny): Remove this from block computation.
     if (dex.hasPayload()) {
       arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex);
     }
     // This instruction does not close the block.
-    return false;
+    return -1;
   }
 
   private boolean inTryRange(Try tryItem, int offset) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index bd8a469..8cd9a2a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -81,9 +81,14 @@
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.StringUtils.BraceType;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntIterator;
+import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -101,6 +106,8 @@
  */
 public class IRBuilder {
 
+  public static final int INITIAL_BLOCK_OFFSET = -1;
+
   // SSA construction uses a worklist of basic blocks reachable from the entry and their
   // instruction offsets.
   private static class WorklistItem {
@@ -158,8 +165,64 @@
     }
   }
 
+  public static class BlockInfo {
+    BasicBlock block = new BasicBlock();
+    IntSet normalPredecessors = new IntArraySet();
+    IntSet normalSuccessors = new IntArraySet();
+    IntSet exceptionalPredecessors = new IntArraySet();
+    IntSet exceptionalSuccessors = new IntArraySet();
+
+    void addNormalPredecessor(int offset) {
+      normalPredecessors.add(offset);
+    }
+
+    void addNormalSuccessor(int offset) {
+      normalSuccessors.add(offset);
+    }
+
+    void replaceNormalPredecessor(int existing, int replacement) {
+      normalPredecessors.remove(existing);
+      normalPredecessors.add(replacement);
+    }
+
+    void addExceptionalPredecessor(int offset) {
+      exceptionalPredecessors.add(offset);
+    }
+
+    void addExceptionalSuccessor(int offset) {
+      exceptionalSuccessors.add(offset);
+    }
+
+    int predecessorCount() {
+      return normalPredecessors.size() + exceptionalPredecessors.size();
+    }
+
+    BlockInfo split(
+        int blockStartOffset, int fallthroughOffset, Int2ReferenceMap<BlockInfo> targets) {
+      BlockInfo fallthroughInfo = new BlockInfo();
+      fallthroughInfo.normalPredecessors = new IntArraySet(Collections.singleton(blockStartOffset));
+      fallthroughInfo.block.incrementUnfilledPredecessorCount();
+      // Move all normal successors to the fallthrough block.
+      IntIterator normalSuccessorIterator = normalSuccessors.iterator();
+      while (normalSuccessorIterator.hasNext()) {
+        BlockInfo normalSuccessor = targets.get(normalSuccessorIterator.nextInt());
+        normalSuccessor.replaceNormalPredecessor(blockStartOffset, fallthroughOffset);
+      }
+      fallthroughInfo.normalSuccessors = normalSuccessors;
+      normalSuccessors = new IntArraySet(Collections.singleton(fallthroughOffset));
+      // Copy all exceptional successors to the fallthrough block.
+      IntIterator exceptionalSuccessorIterator = fallthroughInfo.exceptionalSuccessors.iterator();
+      while (exceptionalSuccessorIterator.hasNext()) {
+        BlockInfo exceptionalSuccessor = targets.get(exceptionalSuccessorIterator.nextInt());
+        exceptionalSuccessor.addExceptionalPredecessor(fallthroughOffset);
+      }
+      fallthroughInfo.exceptionalSuccessors = new IntArraySet(this.exceptionalSuccessors);
+      return fallthroughInfo;
+    }
+  }
+
   // Mapping from instruction offsets to basic-block targets.
-  private final Map<Integer, BasicBlock> targets = new HashMap<>();
+  private final Int2ReferenceSortedMap<BlockInfo> targets = new Int2ReferenceAVLTreeMap<>();
 
   // Worklist of reachable blocks.
   private final Queue<Integer> traceBlocksWorklist = new LinkedList<>();
@@ -219,6 +282,10 @@
     this.options = options;
   }
 
+  public Int2ReferenceSortedMap<BlockInfo> getCFG() {
+    return targets;
+  }
+
   private void addToWorklist(BasicBlock block, int firstInstructionIndex) {
     // TODO(ager): Filter out the ones that are already in the worklist, mark bit in block?
     if (!block.isFilled()) {
@@ -239,13 +306,8 @@
     assert source != null;
     source.setUp();
 
-    // Create entry block.
-    setCurrentBlock(new BasicBlock());
-
-    // If the method needs a prelude, the entry block must never be the target of other blocks.
-    if (!source.needsPrelude()) {
-      targets.put(0, currentBlock);
-    }
+    // Create entry block (at a non-targetable address).
+    targets.put(INITIAL_BLOCK_OFFSET, new BlockInfo());
 
     // Process reachable code paths starting from instruction 0.
     processedInstructions = new boolean[source.instructionCount()];
@@ -260,15 +322,18 @@
       // Process each instruction until the block is closed.
       for (int index = startOfBlockIndex; index < source.instructionCount(); ++index) {
         markIndexProcessed(index);
-        boolean closed = source.traceInstruction(index, this);
-        if (closed) {
+        int closedAt = source.traceInstruction(index, this);
+        if (closedAt != -1) {
+          if (closedAt + 1 < source.instructionCount()) {
+            ensureBlockWithoutEnqueuing(source.instructionOffset(closedAt + 1));
+          }
           break;
         }
         // If the next instruction starts a block, fall through to it.
         if (index + 1 < source.instructionCount()) {
           int nextOffset = source.instructionOffset(index + 1);
-          if (getTarget(nextOffset) != null) {
-            ensureSuccessorBlock(nextOffset);
+          if (targets.get(nextOffset) != null) {
+            ensureNormalSuccessorBlock(startOfBlockOffset, nextOffset);
             break;
           }
         }
@@ -276,6 +341,7 @@
     }
     processedInstructions = null;
 
+    setCurrentBlock(targets.get(INITIAL_BLOCK_OFFSET).block);
     source.buildPrelude(this);
 
     // Process normal blocks reachable from the entry block using a worklist of reachable
@@ -311,9 +377,6 @@
     // necessary.
     splitCriticalEdges();
 
-    // Consistency check.
-    assert phiOperandsAreConsistent();
-
     // Package up the IR code.
     IRCode ir = new IRCode(method, blocks, normalExitBlock, valueNumberGenerator);
 
@@ -340,11 +403,36 @@
 
   private boolean verifyFilledPredecessors() {
     for (BasicBlock block : blocks) {
-      assert block.verifyFilledPredecessors();
+      assert verifyFilledPredecessors(block);
     }
     return true;
   }
 
+  private boolean verifyFilledPredecessors(BasicBlock block) {
+    assert block.verifyFilledPredecessors();
+    // TODO(zerny): Consider moving the validation of the initial control-flow graph to after its
+    // construction and prior to building the IR.
+    for (BlockInfo info : targets.values()) {
+      if (info != null && info.block == block) {
+        assert info.predecessorCount() == block.getPredecessors().size();
+        assert info.normalSuccessors.size() == block.getNormalSucessors().size();
+        if (block.hasCatchHandlers()) {
+          assert info.exceptionalSuccessors.size()
+              == block.getCatchHandlers().getUniqueTargets().size();
+        } else {
+          assert !block.canThrow()
+              || info.exceptionalSuccessors.isEmpty()
+              || (info.exceptionalSuccessors.size() == 1
+                  && info.exceptionalSuccessors.iterator().nextInt() < 0);
+        }
+        return true;
+      }
+    }
+    // There are places where we add in new blocks that we do not represent in the initial CFG.
+    // TODO(zerny): Should we maintain the initial CFG after instruction building?
+    return true;
+  }
+
   private int processWorklist(int blockNumber) {
     for (WorklistItem item = ssaWorklist.poll(); item != null; item = ssaWorklist.poll()) {
       if (item.block.isFilled()) {
@@ -359,11 +447,11 @@
           source.closedCurrentBlock();
           break;
         }
-        BasicBlock block = getTarget(source.instructionOffset(i));
-        if (block != null && block != currentBlock) {
-          closeCurrentBlockWithFallThrough(block);
+        BlockInfo info = targets.get(source.instructionOffset(i));
+        if (info != null && info.block != currentBlock) {
+          closeCurrentBlockWithFallThrough(info.block);
           source.closedCurrentBlockWithFallthrough(i);
-          addToWorklist(block, i);
+          addToWorklist(info.block, i);
           break;
         }
         source.buildInstruction(this, i);
@@ -722,7 +810,7 @@
   public void addGoto(int targetOffset) {
     addInstruction(new Goto());
     BasicBlock targetBlock = getTarget(targetOffset);
-    if (currentBlock.isCatchSuccessor(targetBlock)) {
+    if (currentBlock.hasCatchSuccessor(targetBlock)) {
       needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, targetBlock));
     } else {
       currentBlock.link(targetBlock);
@@ -998,9 +1086,15 @@
 
   public void addMoveException(int dest) {
     Value out = writeRegister(dest, MoveType.OBJECT, ThrowingInfo.NO_THROW);
+    assert out.getDebugInfo() == null;
     MoveException instruction = new MoveException(out);
     assert !instruction.instructionTypeCanThrow();
-    assert currentBlock.getInstructions().isEmpty();
+    if (!currentBlock.getInstructions().isEmpty()) {
+      throw new CompilationError("Invalid MoveException instruction encountered. "
+          + "The MoveException instruction is not the first instruction in the block in "
+          + method.qualifiedName()
+          + ".");
+    }
     addInstruction(instruction);
   }
 
@@ -1151,7 +1245,8 @@
       // If this was a packed switch with only fallthrough cases we can make it a goto.
       // Oddly, this does happen.
       if (numberOfFallthroughs == numberOfTargets) {
-        targets.get(fallthroughOffset).decrementUnfilledPredecessorCount(numberOfFallthroughs);
+        BlockInfo info = targets.get(fallthroughOffset);
+        info.block.decrementUnfilledPredecessorCount(numberOfFallthroughs);
         addGoto(fallthroughOffset);
         return;
       }
@@ -1161,7 +1256,8 @@
       int bytesSaved = packedSwitchPayloadSize - sparseSwitchPayloadSize;
       // Perform the rewrite if we can reduce the payload size by more than 20%.
       if (bytesSaved > (packedSwitchPayloadSize / 5)) {
-        targets.get(fallthroughOffset).decrementUnfilledPredecessorCount(numberOfFallthroughs);
+        BlockInfo info = targets.get(fallthroughOffset);
+        info.block.decrementUnfilledPredecessorCount(numberOfFallthroughs);
         int nextCaseIndex = 0;
         int currentKey = keys[0];
         keys = new int[numberOfSparseTargets];
@@ -1458,10 +1554,16 @@
     BasicBlock block = new BasicBlock();
     blocks.add(block);
     block.incrementUnfilledPredecessorCount();
-    for (Integer offset : source.getCurrentCatchHandlers().getUniqueTargets()) {
-      BasicBlock target = getTarget(offset);
-      assert !target.isSealed();
-      target.incrementUnfilledPredecessorCount();
+    int freshOffset = INITIAL_BLOCK_OFFSET - 1;
+    while (targets.containsKey(freshOffset)) {
+      freshOffset--;
+    }
+    targets.put(freshOffset, null);
+    for (int offset : source.getCurrentCatchHandlers().getUniqueTargets()) {
+      BlockInfo target = targets.get(offset);
+      assert !target.block.isSealed();
+      target.block.incrementUnfilledPredecessorCount();
+      target.addExceptionalPredecessor(freshOffset);
     }
     addInstruction(new Goto());
     currentBlock.link(block);
@@ -1499,21 +1601,32 @@
   // Package (ie, SourceCode accessed) helpers.
 
   // Ensure there is a block starting at offset.
-  BasicBlock ensureBlockWithoutEnqueuing(int offset) {
-    BasicBlock block = targets.get(offset);
-    if (block == null) {
-      block = new BasicBlock();
-      targets.put(offset, block);
+  BlockInfo ensureBlockWithoutEnqueuing(int offset) {
+    assert offset != INITIAL_BLOCK_OFFSET;
+    BlockInfo info = targets.get(offset);
+    if (info == null) {
       // If this is a processed instruction, the block split and it has a fall-through predecessor.
       if (offset >= 0 && isOffsetProcessed(offset)) {
-        block.incrementUnfilledPredecessorCount();
+        int blockStartOffset = getBlockStartOffset(offset);
+        BlockInfo existing = targets.get(blockStartOffset);
+        info = existing.split(blockStartOffset, offset, targets);
+      } else {
+        info = new BlockInfo();
       }
+      targets.put(offset, info);
     }
-    return block;
+    return info;
+  }
+
+  private int getBlockStartOffset(int offset) {
+    if (targets.containsKey(offset)) {
+      return offset;
+    }
+    return targets.headMap(offset).lastIntKey();
   }
 
   // Ensure there is a block starting at offset and add it to the work-list if it needs processing.
-  private BasicBlock ensureBlock(int offset) {
+  private BlockInfo ensureBlock(int offset) {
     // We don't enqueue negative targets (these are special blocks, eg, an argument prelude).
     if (offset >= 0 && !isOffsetProcessed(offset)) {
       traceBlocksWorklist.add(offset);
@@ -1550,16 +1663,32 @@
   }
 
   // Ensure there is a block at offset and add a predecessor to it.
-  BasicBlock ensureSuccessorBlock(int offset) {
-    BasicBlock block = ensureBlock(offset);
-    block.incrementUnfilledPredecessorCount();
-    return block;
+  private void ensureSuccessorBlock(int sourceOffset, int targetOffset, boolean normal) {
+    BlockInfo targetInfo = ensureBlock(targetOffset);
+    int sourceStartOffset = getBlockStartOffset(sourceOffset);
+    BlockInfo sourceInfo = targets.get(sourceStartOffset);
+    if (normal) {
+      sourceInfo.addNormalSuccessor(targetOffset);
+      targetInfo.addNormalPredecessor(sourceStartOffset);
+    } else {
+      sourceInfo.addExceptionalSuccessor(targetOffset);
+      targetInfo.addExceptionalPredecessor(sourceStartOffset);
+    }
+    targetInfo.block.incrementUnfilledPredecessorCount();
+  }
+
+  void ensureNormalSuccessorBlock(int sourceOffset, int targetOffset) {
+    ensureSuccessorBlock(sourceOffset, targetOffset, true);
+  }
+
+  void ensureExceptionalSuccessorBlock(int sourceOffset, int targetOffset) {
+    ensureSuccessorBlock(sourceOffset, targetOffset, false);
   }
 
   // Private block helpers.
 
   private BasicBlock getTarget(int offset) {
-    return targets.get(offset);
+    return targets.get(offset).block;
   }
 
   private void closeCurrentBlock() {
@@ -1577,7 +1706,7 @@
     assert currentBlock != null;
     flushCurrentDebugPosition();
     currentBlock.add(new Goto());
-    if (currentBlock.isCatchSuccessor(nextBlock)) {
+    if (currentBlock.hasCatchSuccessor(nextBlock)) {
       needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, nextBlock));
     } else {
       currentBlock.link(nextBlock);
@@ -1657,8 +1786,8 @@
       target.getPredecessors().add(newBlock);
 
       // Check that the successor indexes are correct.
-      assert source.isCatchSuccessor(newBlock);
-      assert !source.isCatchSuccessor(target);
+      assert source.hasCatchSuccessor(newBlock);
+      assert !source.hasCatchSuccessor(target);
 
       // Mark the filled predecessors to the blocks.
       if (source.isFilled()) {
@@ -1669,22 +1798,6 @@
     return blockNumber;
   }
 
-  private boolean phiOperandsAreConsistent() {
-    for (BasicBlock block : blocks) {
-      if (block.hasIncompletePhis()) {
-        StringBuilder builder = new StringBuilder("Incomplete phis in ");
-        builder.append(method);
-        builder.append(". The following registers appear to be uninitialized: ");
-        StringUtils.append(builder, block.getIncompletePhiRegisters(), ", ", BraceType.NONE);
-        throw new CompilationError(builder.toString());
-      }
-      for (Phi phi : block.getPhis()) {
-        assert phi.getOperands().size() == block.getPredecessors().size();
-      }
-    }
-    return true;
-  }
-
   /**
    * Change to control-flow graph to avoid repeated phi operands when all the same values
    * flow in from multiple predecessors.
@@ -1716,6 +1829,15 @@
   public void joinPredecessorsWithIdenticalPhis() {
     List<BasicBlock> blocksToAdd = new ArrayList<>();
     for (BasicBlock block : blocks) {
+      // Consistency check. At this point there should be no incomplete phis.
+      // If there are, the input is typically dex code that uses a register
+      // that is not defined on all control-flow paths.
+      if (block.hasIncompletePhis()) {
+        throw new CompilationError(
+            "Undefined value encountered during compilation. "
+                + "This is typically caused by invalid dex input that uses a register "
+                + "that is not define on all control-flow paths leading to the use.");
+      }
       if (block.entry() instanceof MoveException) {
         // TODO: Should we support joining in the presence of move-exception instructions?
         continue;
@@ -1768,7 +1890,7 @@
       // If any of the edges to the block are critical, we need to insert new blocks on each
       // containing the move-exception instruction which must remain the first instruction.
       if (block.entry() instanceof MoveException) {
-        block.splitCriticalExceptioEdges(valueNumberGenerator,
+        block.splitCriticalExceptionEdges(valueNumberGenerator,
             newBlock -> {
               newBlock.setNumber(blocks.size() + newBlocks.size());
               newBlocks.add(newBlock);
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 8370eb8..a787025 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
@@ -25,7 +25,7 @@
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
@@ -37,11 +37,10 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
-import java.util.Random;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -66,6 +65,7 @@
   private final LensCodeRewriter lensCodeRewriter;
   private final Inliner inliner;
   private CallGraph callGraph;
+  private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
 
   private DexString highestSortingString;
 
@@ -204,7 +204,7 @@
         boolean matchesMethodFilter = options.methodMatchesFilter(method);
         if (matchesMethodFilter) {
           if (method.getCode().isJarCode()) {
-            rewriteCode(method, Outliner::noProcessing);
+            rewriteCode(method, ignoreOptimizationFeedback, Outliner::noProcessing);
           }
           updateHighestSortingStrings(method);
         }
@@ -236,34 +236,41 @@
 
     // Process the application identifying outlining candidates.
     timing.begin("IR conversion phase 1");
+    int count = 0;
+    OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
+    OptimizationFeedbackDelayed delayedFeedback = new OptimizationFeedbackDelayed();
     while (!callGraph.isEmpty()) {
+      count++;
       CallGraph.Leaves leaves = callGraph.pickLeaves();
-      List<DexEncodedMethod> leafMethods = leaves.getLeaves();
-      assert leafMethods.size() > 0;
-      // If cycles where broken to produce leaves, don't do parallel processing to keep
-      // deterministic output.
-      // TODO(37133161): Most likely the failing:
-      //  java.com.android.tools.r8.internal.R8GMSCoreDeterministicTest
-      // is caused by processing in multiple threads.
-      if (true || leaves.brokeCycles()) {
-        for (DexEncodedMethod method : leafMethods) {
-          processMethod(method,
-              outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
-        }
-      } else {
-        List<Future<?>> futures = new ArrayList<>();
-        // For testing we have the option to randomize the processing order for the
-        // deterministic tests.
-        if (options.testing.randomizeCallGraphLeaves) {
-          Collections.shuffle(leafMethods, new Random(System.nanoTime()));
-        }
-        for (DexEncodedMethod method : leafMethods) {
+      List<DexEncodedMethod> methods = leaves.getLeaves();
+      assert methods.size() > 0;
+      List<Future<?>> futures = new ArrayList<>();
+
+      // For testing we have the option to determine the processing order of the methods.
+      if (options.testing.irOrdering != null) {
+        methods = options.testing.irOrdering.apply(methods, leaves);
+      }
+
+      while (methods.size() > 0) {
+        // Process the methods on multiple threads. If cycles where broken collect the
+        // optimization feedback to reprocess methods affected by it. This is required to get
+        // deterministic behaviour, as the processing order within each set of leaves is
+        // non-deterministic.
+        for (DexEncodedMethod method : methods) {
           futures.add(executorService.submit(() -> {
             processMethod(method,
+                leaves.brokeCycles() ? delayedFeedback : directFeedback,
                 outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
           }));
         }
         ThreadUtils.awaitFutures(futures);
+        if (leaves.brokeCycles()) {
+          // If cycles in the call graph were broken, then re-process all methods which are
+          // affected by the optimization feedback of other methods in this group.
+          methods = delayedFeedback.applyAndClear(methods, leaves);
+        } else {
+          methods = ImmutableList.of();
+        }
       }
     }
     timing.end();
@@ -276,8 +283,7 @@
     if ((inliner != null) && (inliner.doubleInlineCallers.size() > 0)) {
       inliner.applyDoubleInlining = true;
       for (DexEncodedMethod method : inliner.doubleInlineCallers) {
-        method.markNotProcessed();
-        processMethod(method, Outliner::noProcessing);
+        processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
         assert method.isProcessed();
       }
     }
@@ -295,8 +301,7 @@
         for (DexEncodedMethod method : outliner.getMethodsSelectedForOutlining()) {
           // This is the second time we compile this method first mark it not processed.
           assert !method.getCode().isOutlineCode();
-          method.markNotProcessed();
-          processMethod(method, outliner::applyOutliningCandidate);
+          processMethod(method, ignoreOptimizationFeedback, outliner::applyOutliningCandidate);
           assert method.isProcessed();
         }
         builder.addSynthesizedClass(outlineClass, true);
@@ -377,28 +382,28 @@
 
   public void optimizeSynthesizedMethod(DexEncodedMethod method) {
     // Process the generated method, but don't apply any outlining.
-    processMethod(method, Outliner::noProcessing);
+    processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
   }
 
   private String logCode(InternalOptions options, DexEncodedMethod method) {
     return options.useSmaliSyntax ? method.toSmaliString(null) : method.codeToString();
   }
 
-  private void processMethod(
-      DexEncodedMethod method, BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
+  private void processMethod(DexEncodedMethod method,
+      OptimizationFeedback feedback,
+      BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
     Code code = method.getCode();
     boolean matchesMethodFilter = options.methodMatchesFilter(method);
     if (code != null && matchesMethodFilter) {
-      assert !method.isProcessed();
-      InliningConstraint state = rewriteCode(method, outlineHandler);
-      method.markProcessed(state);
+      rewriteCode(method, feedback, outlineHandler);
     } else {
       // Mark abstract methods as processed as well.
-      method.markProcessed(InliningConstraint.NEVER);
+      method.markProcessed(Constraint.NEVER);
     }
   }
 
-  private InliningConstraint rewriteCode(DexEncodedMethod method,
+  private void rewriteCode(DexEncodedMethod method,
+      OptimizationFeedback feedback,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
     if (options.verbose) {
       System.out.println("Processing: " + method.toSourceString());
@@ -409,7 +414,8 @@
     }
     IRCode code = method.buildIR(options);
     if (code == null) {
-      return InliningConstraint.NEVER;
+      feedback.markProcessed(method, Constraint.NEVER);
+      return;
     }
     if (Log.ENABLED) {
       Log.debug(getClass(), "Initial (SSA) flow graph for %s:\n%s", method.toSourceString(), code);
@@ -470,7 +476,7 @@
     }
 
     codeRewriter.shortenLiveRanges(code);
-    codeRewriter.identifyReturnsArgument(method, code);
+    codeRewriter.identifyReturnsArgument(method, code, feedback);
 
     // Insert code to log arguments if requested.
     if (options.methodMatchesLogArgumentsFilter(method)) {
@@ -489,10 +495,13 @@
     printMethod(code, "Final IR (non-SSA)");
 
     // After all the optimizations have take place, we compute whether method should be inlined.
+    Constraint state;
     if (!options.inlineAccessors || inliner == null) {
-      return InliningConstraint.NEVER;
+      state = Constraint.NEVER;
+    } else {
+      state = inliner.identifySimpleMethods(code, method);
     }
-    return inliner.identifySimpleMethods(code, method);
+    feedback.markProcessed(method, state);
   }
 
   private void updateHighestSortingStrings(DexEncodedMethod method) {
@@ -547,7 +556,7 @@
     // Always perform dead code elimination before register allocation. The register allocator
     // does not allow dead code (to make sure that we do not waste registers for unneeded values).
     DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
-    LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code);
+    LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
     registerAllocator.allocateRegisters(options.debug);
     printMethod(code, "After register allocation (non-SSA)");
     printLiveRanges(registerAllocator, "Final live ranges.");
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 5647878..12be2c2 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
@@ -28,17 +28,21 @@
 import com.android.tools.r8.ir.code.MoveType;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Switch;
+import com.android.tools.r8.ir.conversion.IRBuilder.BlockInfo;
 import com.android.tools.r8.ir.conversion.JarState.Local;
 import com.android.tools.r8.ir.conversion.JarState.Slot;
 import com.android.tools.r8.logging.Log;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import org.objectweb.asm.Handle;
@@ -66,7 +70,6 @@
 import org.objectweb.asm.util.Textifier;
 import org.objectweb.asm.util.TraceMethodVisitor;
 
-
 public class JarSourceCode implements SourceCode {
 
   // Try-catch block wrapper containing resolved offsets.
@@ -110,6 +113,16 @@
     }
   }
 
+  private static class JarStateWorklistItem {
+    BlockInfo blockInfo;
+    int instructionIndex;
+
+    public JarStateWorklistItem(BlockInfo blockInfo, int instructionIndex) {
+      this.blockInfo = blockInfo;
+      this.instructionIndex = instructionIndex;
+    }
+  }
+
   // Various descriptors.
   private static final String INT_ARRAY_DESC = "[I";
   private static final String REFLECT_ARRAY_DESC = "Ljava/lang/reflect/Array;";
@@ -144,7 +157,7 @@
   // thus that the monitor-entry prelude (part of the argument block which must not have a try-catch
   // successor) is not joined with the first instruction block (which likely will have a try-catch
   // successor).
-  private static final int EXCEPTIONAL_SYNC_EXIT_OFFSET = -1;
+  private static final int EXCEPTIONAL_SYNC_EXIT_OFFSET = -2;
   private static final TryCatchBlock EXCEPTIONAL_SYNC_EXIT =
       new TryCatchBlock(EXCEPTIONAL_SYNC_EXIT_OFFSET, 0, Integer.MAX_VALUE, null);
 
@@ -295,6 +308,70 @@
         builder.addDebugUninitialized(localRegister, constType(localType));
       }
     }
+    computeBlockEntryJarStates(builder);
+  }
+
+  private void computeBlockEntryJarStates(IRBuilder builder) {
+    Int2ReferenceSortedMap<BlockInfo> CFG = builder.getCFG();
+    Queue<JarStateWorklistItem> worklist = new LinkedList<>();
+    BlockInfo entry = CFG.get(IRBuilder.INITIAL_BLOCK_OFFSET);
+    if (CFG.get(0) != null) {
+      entry = CFG.get(0);
+    }
+    worklist.add(new JarStateWorklistItem(entry, 0));
+    state.recordStateForTarget(0, this);
+    for (JarStateWorklistItem item = worklist.poll(); item != null; item = worklist.poll()) {
+      state.restoreState(item.instructionIndex);
+      // If the block being restored is a try-catch handler push the exception on the stack.
+      for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
+        TryCatchBlockNode tryCatchBlockNode = (TryCatchBlockNode) node.tryCatchBlocks.get(i);
+        if (tryCatchBlockNode.handler == getInstruction(item.instructionIndex)) {
+          state.push(THROWABLE_TYPE);
+          break;
+        }
+      }
+      // Iterate each of the instructions in the block to compute the outgoing JarState.
+      for (int i = item.instructionIndex; i <= instructionCount(); ++i) {
+        // If we are at the end of the instruction stream or if we have reached the start
+        // of a new block, propagate the state to all successors and add the ones
+        // that changed to the worklist.
+        if (i == instructionCount() || (i != item.instructionIndex && CFG.containsKey(i))) {
+          item.blockInfo.normalSuccessors.iterator().forEachRemaining(offset -> {
+            if (state.recordStateForTarget(offset, this)) {
+              if (offset >= 0) {
+                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+              }
+            }
+          });
+          item.blockInfo.exceptionalSuccessors.iterator().forEachRemaining(offset -> {
+            if (state.recordStateForExceptionalTarget(offset, this)) {
+              if (offset >= 0) {
+                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+              }
+            }
+          });
+          break;
+        }
+
+        AbstractInsnNode insn = getInstruction(i);
+        updateState(insn);
+
+        if (!isControlFlowInstruction(insn)) {
+          updateStateForLocalVariableEnd(insn);
+        }
+      }
+    }
+  }
+
+  private void updateStateForLocalVariableEnd(AbstractInsnNode insn) {
+    assert !isControlFlowInstruction(insn);
+    if (!(insn.getNext() instanceof LabelNode)) {
+      return;
+    }
+    // If the label is the end of any local-variable scopes end the locals.
+    LabelNode label = (LabelNode) insn.getNext();
+    List<Local> locals = state.getLocalsToClose(label);
+    state.closeLocals(locals);
   }
 
   @Override
@@ -308,7 +385,6 @@
 
   private void buildExceptionalPostlude(IRBuilder builder) {
     assert isSynchronized();
-    assert state.isInvalid();
     generatingMethodSynchronization = true;
     int exceptionRegister = 0; // We are exiting the method so we just overwrite register 0.
     builder.addMoveException(exceptionRegister);
@@ -324,14 +400,10 @@
 
   @Override
   public void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex) {
-    assert !state.isInvalid();
-    state.recordStateForTarget(fallthroughInstructionIndex, this);
-    state.invalidateState();
   }
 
   @Override
   public void closedCurrentBlock() {
-    assert state.isInvalid();
   }
 
   @Override
@@ -344,8 +416,8 @@
     currentInstruction = insn;
     assert verifyExceptionEdgesAreRecorded(insn);
 
-    // Restore the state if invalid.
-    if (state.isInvalid()) {
+    // If a new block is starting here, we restore the computed JarState.
+    if (builder.getCFG().containsKey(instructionIndex) || instructionIndex == 0) {
       state.restoreState(instructionIndex);
       // If the block being restored is a try-catch handler push the exception on the stack.
       for (int i = 0; i < node.tryCatchBlocks.size(); i++) {
@@ -406,7 +478,7 @@
 
   @Override
   public DebugLocalInfo getCurrentLocal(int register) {
-    return state.getLocalInfoForRegister(register);
+    return generatingMethodSynchronization ? null : state.getLocalInfoForRegister(register);
   }
 
   @Override
@@ -519,24 +591,24 @@
   }
 
   @Override
-  public boolean traceInstruction(int index, IRBuilder builder) {
+  public int traceInstruction(int index, IRBuilder builder) {
     AbstractInsnNode insn = getInstruction(index);
     // Exit early on no-op instructions.
     if (insn instanceof LabelNode || insn instanceof LineNumberNode) {
-      return false;
+      return -1;
     }
     // If this instruction exits, close this block.
     if (isReturn(insn)) {
-      return true;
+      return index;
     }
     // For each target ensure a basic block and close this block.
     int[] targets = getTargets(insn);
     if (targets != NO_TARGETS) {
       assert !canThrow(insn);
       for (int target : targets) {
-        builder.ensureSuccessorBlock(target);
+        builder.ensureNormalSuccessorBlock(index, target);
       }
-      return true;
+      return index;
     }
     if (canThrow(insn)) {
       List<TryCatchBlock> tryCatchBlocks = getTryHandlers(insn);
@@ -549,20 +621,20 @@
           int handler = tryCatchBlock.getHandler();
           if (!seenHandlerOffsets.contains(handler)) {
             seenHandlerOffsets.add(handler);
-            builder.ensureSuccessorBlock(handler);
+            builder.ensureExceptionalSuccessorBlock(index, handler);
           }
         }
         // Edge to normal successor if any (fallthrough).
         if (!isThrow(insn)) {
-          builder.ensureSuccessorBlock(getOffset(insn.getNext()));
+          builder.ensureNormalSuccessorBlock(index, getOffset(insn.getNext()));
         }
-        return true;
+        return index;
       }
       // If the throwable instruction is "throw" it closes the block.
-      return isThrow(insn);
+      return isThrow(insn) ? index : -1;
     }
     // This instruction does not close the block.
-    return false;
+    return -1;
   }
 
   private List<TryCatchBlock> getPotentialTryHandlers(AbstractInsnNode insn) {
@@ -988,6 +1060,674 @@
     }
   }
 
+  // State updating procedures.
+
+  private void updateState(AbstractInsnNode insn) {
+    switch (insn.getType()) {
+      case AbstractInsnNode.INSN:
+        updateState((InsnNode) insn);
+        break;
+      case AbstractInsnNode.INT_INSN:
+        updateState((IntInsnNode) insn);
+        break;
+      case AbstractInsnNode.VAR_INSN:
+        updateState((VarInsnNode) insn);
+        break;
+      case AbstractInsnNode.TYPE_INSN:
+        updateState((TypeInsnNode) insn);
+        break;
+      case AbstractInsnNode.FIELD_INSN:
+        updateState((FieldInsnNode) insn);
+        break;
+      case AbstractInsnNode.METHOD_INSN:
+        updateState((MethodInsnNode) insn);
+        break;
+      case AbstractInsnNode.INVOKE_DYNAMIC_INSN:
+        updateState((InvokeDynamicInsnNode) insn);
+        break;
+      case AbstractInsnNode.JUMP_INSN:
+        updateState((JumpInsnNode) insn);
+        break;
+      case AbstractInsnNode.LABEL:
+        updateState((LabelNode) insn);
+        break;
+      case AbstractInsnNode.LDC_INSN:
+        updateState((LdcInsnNode) insn);
+        break;
+      case AbstractInsnNode.IINC_INSN:
+        updateState((IincInsnNode) insn);
+        break;
+      case AbstractInsnNode.TABLESWITCH_INSN:
+        updateState((TableSwitchInsnNode) insn);
+        break;
+      case AbstractInsnNode.LOOKUPSWITCH_INSN:
+        updateState((LookupSwitchInsnNode) insn);
+        break;
+      case AbstractInsnNode.MULTIANEWARRAY_INSN:
+        updateState((MultiANewArrayInsnNode) insn);
+        break;
+      case AbstractInsnNode.LINE:
+        updateState((LineNumberNode) insn);
+        break;
+      default:
+        throw new Unreachable("Unexpected instruction " + insn);
+    }
+  }
+
+  private void updateState(InsnNode insn) {
+    int opcode = insn.getOpcode();
+    switch (opcode) {
+      case Opcodes.NOP:
+        // Intentionally left empty.
+        break;
+      case Opcodes.ACONST_NULL:
+        state.push(JarState.NULL_TYPE);
+        break;
+      case Opcodes.ICONST_M1:
+      case Opcodes.ICONST_0:
+      case Opcodes.ICONST_1:
+      case Opcodes.ICONST_2:
+      case Opcodes.ICONST_3:
+      case Opcodes.ICONST_4:
+      case Opcodes.ICONST_5:
+        state.push(Type.INT_TYPE);
+        break;
+      case Opcodes.LCONST_0:
+      case Opcodes.LCONST_1:
+        state.push(Type.LONG_TYPE);
+        break;
+      case Opcodes.FCONST_0:
+      case Opcodes.FCONST_1:
+      case Opcodes.FCONST_2:
+        state.push(Type.FLOAT_TYPE);
+        break;
+      case Opcodes.DCONST_0:
+      case Opcodes.DCONST_1:
+        state.push(Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.IALOAD:
+      case Opcodes.LALOAD:
+      case Opcodes.FALOAD:
+      case Opcodes.DALOAD:
+      case Opcodes.AALOAD:
+      case Opcodes.BALOAD:
+      case Opcodes.CALOAD:
+      case Opcodes.SALOAD: {
+        state.pop();
+        Slot array = state.pop(JarState.ARRAY_TYPE);
+        Type elementType = getArrayElementType(array.type);
+        if (elementType == null) {
+          // We propagate the null type, which will then get resolved to an
+          // actual type if we have a non-null type on another flow edge.
+          elementType = JarState.NULL_TYPE;
+        }
+        state.push(elementType);
+        break;
+      }
+      case Opcodes.IASTORE:
+      case Opcodes.LASTORE:
+      case Opcodes.FASTORE:
+      case Opcodes.DASTORE:
+      case Opcodes.AASTORE:
+      case Opcodes.BASTORE:
+      case Opcodes.CASTORE:
+      case Opcodes.SASTORE: {
+        state.pop();
+        state.pop();
+        state.pop();
+        break;
+      }
+      case Opcodes.POP: {
+        Slot value = state.pop();
+        assert value.isCategory1();
+        break;
+      }
+      case Opcodes.POP2: {
+        Slot value = state.pop();
+        if (value.isCategory1()) {
+          Slot value2 = state.pop();
+          assert value2.isCategory1();
+        }
+        break;
+      }
+      case Opcodes.DUP: {
+        Slot value = state.peek();
+        assert value.isCategory1();
+        state.push(value.type);
+        break;
+      }
+      case Opcodes.DUP_X1: {
+        // Stack transformation: ..., v2, v1 -> ..., v1, v2, v1
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1() && value2.isCategory1();
+        int stack2 = state.push(value1.type);
+        int stack1 = state.push(value2.type);
+        state.push(value1.type);
+        assert value2.register == stack2;
+        assert value1.register == stack1;
+        break;
+      }
+      case Opcodes.DUP_X2: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1();
+        if (value2.isCategory1()) {
+          Slot value3 = state.pop();
+          assert value3.isCategory1();
+          // Stack transformation: ..., v3, v2, v1 -> ..., v1, v3, v2, v1
+          updateStateForDupOneBelowTwo(value3, value2, value1);
+        } else {
+          // Stack transformation: ..., w2, v1 -> ..., v1, w2, v1
+          updateStateForDupOneBelowOne(value2, value1);
+        }
+        break;
+      }
+      case Opcodes.DUP2: {
+        Slot value1 = state.pop();
+        if (value1.isCategory1()) {
+          Slot value2 = state.pop();
+          // Stack transformation: ..., v2, v1 -> ..., v2, v1, v2, v1
+          assert value2.isCategory1();
+          state.push(value2.type);
+          state.push(value1.type);
+          state.push(value2.type);
+          state.push(value1.type);
+        } else {
+          // Stack transformation: ..., w1 -> ..., w1, w1
+          state.push(value1.type);
+          state.push(value1.type);
+        }
+        break;
+      }
+      case Opcodes.DUP2_X1: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value2.isCategory1();
+        if (value1.isCategory1()) {
+          // Stack transformation: ..., v3, v2, v1 -> v2, v1, v3, v2, v1
+          Slot value3 = state.pop();
+          assert value3.isCategory1();
+          updateStateForDupTwoBelowOne(value3, value2, value1);
+        } else {
+          // Stack transformation: ..., v2, w1 -> ..., w1, v2, w1
+          updateStateForDupOneBelowOne(value2, value1);
+        }
+        break;
+      }
+      case Opcodes.DUP2_X2: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        if (!value1.isCategory1() && !value2.isCategory1()) {
+          // State transformation: ..., w2, w1 -> w1, w2, w1
+          updateStateForDupOneBelowOne(value2, value1);
+        } else {
+          Slot value3 = state.pop();
+          if (!value1.isCategory1()) {
+            assert value2.isCategory1();
+            assert value3.isCategory1();
+            // State transformation: ..., v3, v2, w1 -> w1, v3, v2, w1
+            updateStateForDupOneBelowTwo(value3, value2, value1);
+          } else if (!value3.isCategory1()) {
+            assert value1.isCategory1();
+            assert value2.isCategory1();
+            // State transformation: ..., w3, v2, v1 -> v2, v1, w3, v2, v1
+            updateStateForDupTwoBelowOne(value3, value2, value1);
+          } else {
+            Slot value4 = state.pop();
+            assert value1.isCategory1();
+            assert value2.isCategory1();
+            assert value3.isCategory1();
+            assert value4.isCategory1();
+            // State transformation: ..., v4, v3, v2, v1 -> v2, v1, v4, v3, v2, v1
+            updateStateForDupTwoBelowTwo(value4, value3, value2, value1);
+          }
+        }
+        break;
+      }
+      case Opcodes.SWAP: {
+        Slot value1 = state.pop();
+        Slot value2 = state.pop();
+        assert value1.isCategory1() && value2.isCategory1();
+        state.push(value1.type);
+        state.push(value2.type);
+        break;
+      }
+      case Opcodes.IADD:
+      case Opcodes.LADD:
+      case Opcodes.FADD:
+      case Opcodes.DADD:
+      case Opcodes.ISUB:
+      case Opcodes.LSUB:
+      case Opcodes.FSUB:
+      case Opcodes.DSUB:
+      case Opcodes.IMUL:
+      case Opcodes.LMUL:
+      case Opcodes.FMUL:
+      case Opcodes.DMUL:
+      case Opcodes.IDIV:
+      case Opcodes.LDIV:
+      case Opcodes.FDIV:
+      case Opcodes.DDIV:
+      case Opcodes.IREM:
+      case Opcodes.LREM:
+      case Opcodes.FREM:
+      case Opcodes.DREM: {
+        Type type = opType(opcode);
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.INEG:
+      case Opcodes.LNEG:
+      case Opcodes.FNEG:
+      case Opcodes.DNEG: {
+        Type type = opType(opcode);
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.ISHL:
+      case Opcodes.LSHL:
+      case Opcodes.ISHR:
+      case Opcodes.LSHR:
+      case Opcodes.IUSHR:
+      case Opcodes.LUSHR: {
+        Type type = opType(opcode);
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IAND:
+      case Opcodes.LAND: {
+        Type type = opcode == Opcodes.IAND ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IOR:
+      case Opcodes.LOR: {
+        Type type = opcode == Opcodes.IOR ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.IXOR:
+      case Opcodes.LXOR: {
+        Type type = opcode == Opcodes.IXOR ? Type.INT_TYPE : Type.LONG_TYPE;
+        state.pop();
+        state.pop();
+        state.push(type);
+        break;
+      }
+      case Opcodes.I2L:
+        updateStateForConversion(Type.INT_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.I2F:
+        updateStateForConversion(Type.INT_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.I2D:
+        updateStateForConversion(Type.INT_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.L2I:
+        updateStateForConversion(Type.LONG_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.L2F:
+        updateStateForConversion(Type.LONG_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.L2D:
+        updateStateForConversion(Type.LONG_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.F2I:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.F2L:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.F2D:
+        updateStateForConversion(Type.FLOAT_TYPE, Type.DOUBLE_TYPE);
+        break;
+      case Opcodes.D2I:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.INT_TYPE);
+        break;
+      case Opcodes.D2L:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.LONG_TYPE);
+        break;
+      case Opcodes.D2F:
+        updateStateForConversion(Type.DOUBLE_TYPE, Type.FLOAT_TYPE);
+        break;
+      case Opcodes.I2B:
+        updateStateForConversion(Type.INT_TYPE, Type.BYTE_TYPE);
+        break;
+      case Opcodes.I2C:
+        updateStateForConversion(Type.INT_TYPE, Type.CHAR_TYPE);
+        break;
+      case Opcodes.I2S:
+        updateStateForConversion(Type.INT_TYPE, Type.SHORT_TYPE);
+        break;
+      case Opcodes.LCMP: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.FCMPL:
+      case Opcodes.FCMPG: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.DCMPL:
+      case Opcodes.DCMPG: {
+        state.pop();
+        state.pop();
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.IRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.LRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.FRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.DRETURN: {
+        state.pop();
+        break;
+      }
+      case Opcodes.ARETURN: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      case Opcodes.RETURN: {
+        break;
+      }
+      case Opcodes.ARRAYLENGTH: {
+        state.pop(JarState.ARRAY_TYPE);
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.ATHROW: {
+        state.pop(JarState.OBJECT_TYPE);
+        break;
+      }
+      case Opcodes.MONITORENTER: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      case Opcodes.MONITOREXIT: {
+        state.pop(JarState.REFERENCE_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected Insn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateStateForDupOneBelowTwo(Slot value3, Slot value2, Slot value1) {
+    state.push(value1.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupOneBelowOne(Slot value2, Slot value1) {
+    state.push(value1.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupTwoBelowOne(Slot value3, Slot value2, Slot value1) {
+    state.push(value2.type);
+    state.push(value1.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateStateForDupTwoBelowTwo(Slot value4, Slot value3, Slot value2, Slot value1) {
+    state.push(value2.type);
+    state.push(value1.type);
+    state.push(value4.type);
+    state.push(value3.type);
+    state.push(value2.type);
+    state.push(value1.type);
+  }
+
+  private void updateState(IntInsnNode insn) {
+    switch (insn.getOpcode()) {
+      case Opcodes.BIPUSH:
+      case Opcodes.SIPUSH: {
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      case Opcodes.NEWARRAY: {
+        String desc = arrayTypeDesc(insn.operand);
+        Type type = Type.getType(desc);
+        state.pop();
+        state.push(type);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected IntInsn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateState(VarInsnNode insn) {
+    int opcode = insn.getOpcode();
+    Type expectedType;
+    switch (opcode) {
+      case Opcodes.ILOAD:
+      case Opcodes.ISTORE:
+        expectedType = Type.INT_TYPE;
+        break;
+      case Opcodes.FLOAD:
+      case Opcodes.FSTORE:
+        expectedType = Type.FLOAT_TYPE;
+        break;
+      case Opcodes.LLOAD:
+      case Opcodes.LSTORE:
+        expectedType = Type.LONG_TYPE;
+        break;
+      case Opcodes.DLOAD:
+      case Opcodes.DSTORE:
+        expectedType = Type.DOUBLE_TYPE;
+        break;
+      case Opcodes.ALOAD:
+      case Opcodes.ASTORE:
+        expectedType = JarState.REFERENCE_TYPE;
+        break;
+      case Opcodes.RET: {
+        throw new Unreachable("RET should be handled by the ASM jsr inliner");
+      }
+      default:
+        throw new Unreachable("Unexpected VarInsn opcode: " + insn.getOpcode());
+    }
+    if (Opcodes.ILOAD <= opcode && opcode <= Opcodes.ALOAD) {
+      Slot src = state.readLocal(insn.var, expectedType);
+      state.push(src.type);
+    } else {
+      assert Opcodes.ISTORE <= opcode && opcode <= Opcodes.ASTORE;
+      Slot slot = state.pop();
+      if (slot.type == JarState.NULL_TYPE && expectedType != JarState.REFERENCE_TYPE) {
+        state.writeLocal(insn.var, expectedType);
+      } else {
+        state.writeLocal(insn.var, slot.type);
+      }
+    }
+  }
+
+  private void updateState(TypeInsnNode insn) {
+    Type type = Type.getObjectType(insn.desc);
+    switch (insn.getOpcode()) {
+      case Opcodes.NEW: {
+        state.push(type);
+        break;
+      }
+      case Opcodes.ANEWARRAY: {
+        Type arrayType = makeArrayType(type);
+        state.pop();
+        state.push(arrayType);
+        break;
+      }
+      case Opcodes.CHECKCAST: {
+        // Pop the top value and push it back on with the checked type.
+        state.pop(type);
+        state.push(type);
+        break;
+      }
+      case Opcodes.INSTANCEOF: {
+        state.pop(JarState.REFERENCE_TYPE);
+        state.push(Type.INT_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected TypeInsn opcode: " + insn.getOpcode());
+    }
+
+  }
+
+  private void updateState(FieldInsnNode insn) {
+    Type type = Type.getType(insn.desc);
+    switch (insn.getOpcode()) {
+      case Opcodes.GETSTATIC:
+        state.push(type);
+        break;
+      case Opcodes.PUTSTATIC:
+        state.pop();
+        break;
+      case Opcodes.GETFIELD: {
+        state.pop(JarState.OBJECT_TYPE);
+        state.push(type);
+        break;
+      }
+      case Opcodes.PUTFIELD: {
+        state.pop();
+        state.pop(JarState.OBJECT_TYPE);
+        break;
+      }
+      default:
+        throw new Unreachable("Unexpected FieldInsn opcode: " + insn.getOpcode());
+    }
+  }
+
+  private void updateState(MethodInsnNode insn) {
+    updateStateForInvoke(insn.desc, insn.getOpcode() != Opcodes.INVOKESTATIC);
+  }
+
+  private void updateState(InvokeDynamicInsnNode insn) {
+    updateStateForInvoke(insn.desc, false /* receiver passed explicitly */);
+  }
+
+  private void updateStateForInvoke(String desc, boolean implicitReceiver) {
+    // Pop arguments.
+    Type[] parameterTypes = Type.getArgumentTypes(desc);
+    state.popReverse(parameterTypes.length);
+    // Pop implicit receiver if needed.
+    if (implicitReceiver) {
+      state.pop();
+    }
+    // Push return value if needed.
+    Type returnType = Type.getReturnType(desc);
+    if (returnType != Type.VOID_TYPE) {
+      state.push(returnType);
+    }
+  }
+
+  private void updateState(JumpInsnNode insn) {
+    int[] targets = getTargets(insn);
+    int opcode = insn.getOpcode();
+    if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
+      assert targets.length == 2;
+      if (opcode <= Opcodes.IFLE) {
+        state.pop();
+      } else {
+        state.pop();
+        state.pop();
+      }
+    } else {
+      switch (opcode) {
+        case Opcodes.GOTO: {
+          assert targets.length == 1;
+          break;
+        }
+        case Opcodes.IFNULL:
+        case Opcodes.IFNONNULL: {
+          state.pop();
+          break;
+        }
+        case Opcodes.JSR: {
+          throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+        }
+        default:
+          throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
+      }
+    }
+  }
+
+  private void updateState(LabelNode insn) {
+    // Open the scope of locals starting at this point.
+    if (insn != initialLabel) {
+      state.openLocals(insn);
+    }
+  }
+
+  private void updateState(LdcInsnNode insn) {
+    if (insn.cst instanceof Type) {
+      Type type = (Type) insn.cst;
+      state.push(type);
+    } else if (insn.cst instanceof String) {
+      state.push(STRING_TYPE);
+    } else if (insn.cst instanceof Long) {
+      state.push(Type.LONG_TYPE);
+    } else if (insn.cst instanceof Double) {
+      state.push(Type.DOUBLE_TYPE);
+    } else if (insn.cst instanceof Integer) {
+      state.push(Type.INT_TYPE);
+    } else {
+      assert insn.cst instanceof Float;
+      state.push(Type.FLOAT_TYPE);
+    }
+  }
+
+  private void updateState(IincInsnNode insn) {
+    state.readLocal(insn.var, Type.INT_TYPE);
+  }
+
+  private void updateState(TableSwitchInsnNode insn) {
+    state.pop();
+  }
+
+  private void updateState(LookupSwitchInsnNode insn) {
+    state.pop();
+  }
+
+  private void updateState(MultiANewArrayInsnNode insn) {
+    // Type of the full array.
+    Type arrayType = Type.getObjectType(insn.desc);
+    state.popReverse(insn.dims, Type.INT_TYPE);
+    state.push(arrayType);
+  }
+
+  private void updateState(LineNumberNode insn) {
+    // Intentionally empty.
+  }
+
+  private void updateStateForConversion(Type from, Type to) {
+    state.pop();
+    state.push(to);
+  }
+
   // IR instruction building procedures.
 
   private void build(AbstractInsnNode insn, IRBuilder builder) {
@@ -1510,7 +2250,6 @@
       processLocalVariablesAtControlEdge(insn, builder);
     }
     builder.addThrow(register);
-    state.invalidateState();
   }
 
   private void addReturn(InsnNode insn, MoveType type, int register, IRBuilder builder) {
@@ -1521,7 +2260,6 @@
     } else {
       builder.addReturn(type, register);
     }
-    state.invalidateState();
   }
 
   private void dupOneBelowTwo(Slot value3, Slot value2, Slot value1, IRBuilder builder) {
@@ -1914,10 +2652,6 @@
           throw new Unreachable("Unexpected JumpInsn opcode: " + insn.getOpcode());
       }
     }
-    for (int target : targets) {
-      state.recordStateForTarget(target, this);
-    }
-    state.invalidateState();
   }
 
   private void build(LabelNode insn, IRBuilder builder) {
@@ -1927,11 +2661,6 @@
         builder.addDebugLocalStart(local.slot.register, local.info);
       }
     }
-    // Processing local-variable ends is done before closing potential control-flow edges.
-    // Record the state for all the try-catch handlers that are active at this label.
-    for (TryCatchBlock tryCatchBlock : getTryHandlers(insn)) {
-      state.recordStateForExceptionalTarget(tryCatchBlock.getHandler(), this);
-    }
   }
 
   private void build(LdcInsnNode insn, IRBuilder builder) {
@@ -1969,6 +2698,7 @@
   }
 
   private void build(LookupSwitchInsnNode insn, IRBuilder builder) {
+    processLocalVariablesAtControlEdge(insn, builder);
     int[] keys = new int[insn.keys.size()];
     for (int i = 0; i < insn.keys.size(); i++) {
       keys[i] = (int) insn.keys.get(i);
@@ -1980,15 +2710,12 @@
       IRBuilder builder) {
     int index = state.pop(Type.INT_TYPE).register;
     int fallthroughOffset = getOffset(dflt);
-    state.recordStateForTarget(fallthroughOffset, this);
     int[] labelOffsets = new int[labels.size()];
     for (int i = 0; i < labels.size(); i++) {
       int offset = getOffset((LabelNode) labels.get(i));
       labelOffsets[i] = offset;
-      state.recordStateForTarget(offset, this);
     }
     builder.addSwitch(type, index, keys, fallthroughOffset, labelOffsets);
-    state.invalidateState();
   }
 
   private void build(MultiANewArrayInsnNode insn, IRBuilder builder) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index e45fcb2..51a8117 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -40,13 +40,6 @@
   // Type representative for an address type (used by JSR/RET).
   public static final Type ADDR_TYPE = Type.getObjectType("<address>");
 
-  private static final Slot INVALID_SLOT = new Slot(-1, null);
-  private static final Local INVALID_LOCAL = new Local(INVALID_SLOT, null);
-
-  public boolean isInvalid() {
-    return stack.peek() == INVALID_SLOT;
-  }
-
   // Typed mapping from a local slot or stack slot to a virtual register.
   public static class Slot {
     public final int register;
@@ -54,7 +47,7 @@
 
     @Override
     public String toString() {
-      return this == INVALID_SLOT ? "<invalid slot>" : "r" + register + ":" + type;
+      return "r" + register + ":" + type;
     }
 
     public Slot(int register, Type type) {
@@ -356,15 +349,6 @@
 
   // State procedures.
 
-  public void invalidateState() {
-    for (int i = 0; i < locals.length; i++) {
-      locals[i] = INVALID_LOCAL;
-    }
-    stack.clear();
-    stack.push(INVALID_SLOT);
-    topOfStack = -1;
-  }
-
   public boolean hasState(int offset) {
     return targetStates.get(offset) != null;
   }
@@ -374,49 +358,100 @@
     assert snapshot != null;
     assert locals.length == snapshot.locals.length;
     for (int i = 0; i < locals.length; i++) {
-      assert locals[i] == INVALID_LOCAL;
       locals[i] = snapshot.locals[i];
     }
-    assert stack.peek() == INVALID_SLOT;
-    assert topOfStack == -1;
     stack.clear();
     stack.addAll(snapshot.stack);
     topOfStack = startOfStack + 2 * stack.size();
   }
 
-  public void recordStateForTarget(int target, JarSourceCode source) {
-    recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
+  public boolean recordStateForTarget(int target, JarSourceCode source) {
+    return recordStateForTarget(target, locals.clone(), ImmutableList.copyOf(stack), source);
   }
 
-  public void recordStateForExceptionalTarget(int target, JarSourceCode source) {
-    recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
+  public boolean recordStateForExceptionalTarget(int target, JarSourceCode source) {
+    return recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
   }
 
-  private void recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
+  private boolean recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
       JarSourceCode source) {
-    Snapshot snapshot = targetStates.get(target);
-    if (snapshot == null) {
-      if (!localVariables.isEmpty()) {
-        for (int i = 0; i < locals.length; i++) {
-          if (locals[i] != null) {
-            locals[i] = new Local(locals[i].slot, null);
-          }
-        }
-        // TODO(zerny): Precompute and sort the local ranges.
-        for (LocalVariableNode node : localVariables.keySet()) {
-          int startOffset = source.getOffset(node.start);
-          int endOffset = source.getOffset(node.end);
-          if (startOffset <= target && target < endOffset) {
-            int register = getLocalRegister(node.index, Type.getType(node.desc));
-            Local local = locals[register];
-            locals[register] = new Local(local.slot, localVariables.get(node));
-          }
+    if (!localVariables.isEmpty()) {
+      for (int i = 0; i < locals.length; i++) {
+        if (locals[i] != null) {
+          locals[i] = new Local(locals[i].slot, null);
         }
       }
-      targetStates.put(target, new Snapshot(locals, stack));
-      return;
+      // TODO(zerny): Precompute and sort the local ranges.
+      for (LocalVariableNode node : localVariables.keySet()) {
+        int startOffset = source.getOffset(node.start);
+        int endOffset = source.getOffset(node.end);
+        if (startOffset <= target && target < endOffset) {
+          int register = getLocalRegister(node.index, Type.getType(node.desc));
+          Local local = locals[register];
+          locals[register] = new Local(local.slot, localVariables.get(node));
+        }
+      }
     }
-    assert verifyStack(stack, snapshot.stack);
+    Snapshot snapshot = targetStates.get(target);
+    if (snapshot != null) {
+      Local[] newLocals = mergeLocals(snapshot.locals, locals);
+      ImmutableList<Slot> newStack = mergeStacks(snapshot.stack, stack);
+      if (newLocals != snapshot.locals || newStack != snapshot.stack) {
+        targetStates.put(target, new Snapshot(newLocals, newStack));
+        return true;
+      }
+      // The snapshot is up to date - no new type information recoded.
+      return false;
+    }
+    targetStates.put(target, new Snapshot(locals, stack));
+    return true;
+  }
+
+  private ImmutableList<Slot> mergeStacks(
+      ImmutableList<Slot> currentStack, ImmutableList<Slot> newStack) {
+    assert currentStack.size() == newStack.size();
+    List<Slot> mergedStack = null;
+    for (int i = 0; i < currentStack.size(); i++) {
+      if (currentStack.get(i).type == JarState.NULL_TYPE &&
+          newStack.get(i).type != JarState.NULL_TYPE) {
+        if (mergedStack == null) {
+          mergedStack = new ArrayList<>();
+          mergedStack.addAll(currentStack.subList(0, i));
+        }
+        mergedStack.add(newStack.get(i));
+      } else if (mergedStack != null) {
+        assert currentStack.get(i).isCompatibleWith(newStack.get(i).type);
+        mergedStack.add(currentStack.get(i));
+      }
+    }
+    return mergedStack != null ? ImmutableList.copyOf(mergedStack) : currentStack;
+  }
+
+  private Local[] mergeLocals(Local[] currentLocals, Local[] newLocals) {
+    assert currentLocals.length == newLocals.length;
+    Local[] mergedLocals = null;
+    for (int i = 0; i < currentLocals.length; i++) {
+      Local currentLocal = currentLocals[i];
+      Local newLocal = newLocals[i];
+      if (currentLocal == null || newLocal == null) {
+        continue;
+      }
+      // If this assert triggers we can get different debug information for the same local
+      // on different control-flow paths and we will have to merge them.
+      assert currentLocal.info == newLocal.info;
+      if (currentLocal.slot.type == JarState.NULL_TYPE &&
+          newLocal.slot.type != JarState.NULL_TYPE) {
+        if (mergedLocals == null) {
+          mergedLocals = new Local[currentLocals.length];
+          System.arraycopy(currentLocals, 0, mergedLocals, 0, i);
+        }
+        Slot newSlot = new Slot(newLocal.slot.register, newLocal.slot.type);
+        mergedLocals[i] = new Local(newSlot, newLocal.info);
+      } else if (mergedLocals != null) {
+        mergedLocals[i] = currentLocals[i];
+      }
+    }
+    return mergedLocals != null ? mergedLocals : currentLocals;
   }
 
   private static boolean verifyStack(List<Slot> stack, List<Slot> other) {
@@ -446,9 +481,6 @@
   public static String stackToString(Collection<Slot> stack) {
     List<String> strings = new ArrayList<>(stack.size());
     for (Slot slot : stack) {
-      if (slot == INVALID_SLOT) {
-        return "<stack invalidated>";
-      }
       strings.add(slot.type.toString());
     }
     StringBuilder builder = new StringBuilder("{ ");
@@ -466,9 +498,6 @@
     StringBuilder builder = new StringBuilder("{ ");
     boolean first = true;
     for (Local local : locals) {
-      if (local == INVALID_LOCAL) {
-        return "<locals invalidated>";
-      }
       if (!first) {
         builder.append(", ");
       } else {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
new file mode 100644
index 0000000..be5718e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -0,0 +1,15 @@
+// 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.ir.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public interface OptimizationFeedback {
+  void methodReturnsArgument(DexEncodedMethod method, int argument);
+  void methodReturnsConstant(DexEncodedMethod method, long value);
+  void methodNeverReturnsNull(DexEncodedMethod method);
+  void markProcessed(DexEncodedMethod method, Constraint state);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
new file mode 100644
index 0000000..25d3fe5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
@@ -0,0 +1,117 @@
+// 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.ir.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongMap;
+import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class OptimizationFeedbackDelayed implements OptimizationFeedback {
+
+  private Reference2IntMap<DexEncodedMethod> returnsArgument = new Reference2IntOpenHashMap<>();
+  private Reference2LongMap<DexEncodedMethod> returnsConstant = new Reference2LongOpenHashMap<>();
+  private Set<DexEncodedMethod> neverReturnsNull = Sets.newIdentityHashSet();
+  private Map<DexEncodedMethod, Constraint> inliningConstraints = Maps.newIdentityHashMap();
+
+  @Override
+  synchronized public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+    if (method.getOptimizationInfo().returnsArgument()) {
+      assert method.getOptimizationInfo().getReturnedArgument() == argument;
+      return;
+    }
+    assert !returnsArgument.containsKey(method);
+    returnsArgument.put(method, argument);
+  }
+
+  @Override
+  synchronized public void methodReturnsConstant(DexEncodedMethod method, long value) {
+    if (method.getOptimizationInfo().returnsConstant()) {
+      assert method.getOptimizationInfo().getReturnedConstant() == value;
+      return;
+    }
+    assert !returnsConstant.containsKey(method);
+    returnsConstant.put(method, value);
+  }
+
+  @Override
+  synchronized public void methodNeverReturnsNull(DexEncodedMethod method) {
+    if (method.getOptimizationInfo().neverReturnsNull()) {
+      return;
+    }
+    assert !neverReturnsNull.contains(method);
+    neverReturnsNull.add(method);
+  }
+
+  @Override
+  public void markProcessed(DexEncodedMethod method, Constraint state) {
+    if (state == Constraint.NEVER) {
+      assert method.cannotInline();
+      method.markProcessed(state);
+    } else {
+      assert method.cannotInline();
+      inliningConstraints.put(method, state);
+    }
+  }
+
+  private <T> boolean setsOverlap(Set<T> set1, Set<T> set2) {
+    for (T element : set1) {
+      if (set2.contains(element)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Apply the optimization feedback.
+   *
+   * Returns the methods from the passed in list that could be affected by applying the
+   * optimization feedback.
+   */
+  public List<DexEncodedMethod> applyAndClear(
+      List<DexEncodedMethod> processed, CallGraph.Leaves leaves) {
+    returnsArgument.forEach(DexEncodedMethod::markReturnsArgument);
+    returnsConstant.forEach(DexEncodedMethod::markReturnsConstant);
+    neverReturnsNull.forEach(DexEncodedMethod::markNeverReturnsNull);
+
+    // Collect all methods affected by the optimization feedback applied.
+    Set<DexEncodedMethod> all = Sets.newIdentityHashSet();
+    all.addAll(returnsArgument.keySet());
+    all.addAll(returnsConstant.keySet());
+    all.addAll(neverReturnsNull);
+    inliningConstraints.forEach((method, constraint) -> {
+      boolean changed = method.markProcessed(constraint);
+      if (changed) {
+        all.add(method);
+      }
+    });
+
+    // Collect the processed methods which could be affected by the applied optimization feedback.
+    List<DexEncodedMethod> result = new ArrayList<>();
+    for (DexEncodedMethod method : processed) {
+      Set<DexEncodedMethod> calls = leaves.getCycleBreakingCalls().get(method);
+      if (setsOverlap(calls, all)) {
+        result.add(method);
+      }
+    }
+
+    // Clear the collected optimization feedback.
+    returnsArgument.clear();
+    returnsConstant.clear();
+    neverReturnsNull.clear();
+    inliningConstraints.clear();
+
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
new file mode 100644
index 0000000..dc0a809
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -0,0 +1,31 @@
+// 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.ir.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public class OptimizationFeedbackDirect implements OptimizationFeedback {
+
+  @Override
+  public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+    method.markReturnsArgument(argument);
+  }
+
+  @Override
+  public void methodReturnsConstant(DexEncodedMethod method, long value) {
+    method.markReturnsConstant(value);
+  }
+
+  @Override
+  public void methodNeverReturnsNull(DexEncodedMethod method) {
+    method.markNeverReturnsNull();
+  }
+
+  @Override
+  public void markProcessed(DexEncodedMethod method, Constraint state) {
+    method.markProcessed(state);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
new file mode 100644
index 0000000..3c8057f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -0,0 +1,23 @@
+// 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.ir.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public class OptimizationFeedbackIgnore implements OptimizationFeedback {
+
+  @Override
+  public void methodReturnsArgument(DexEncodedMethod method, int argument) {}
+
+  @Override
+  public void methodReturnsConstant(DexEncodedMethod method, long value) {}
+
+  @Override
+  public void methodNeverReturnsNull(DexEncodedMethod method) {}
+
+  @Override
+  public void markProcessed(DexEncodedMethod method, Constraint state) {}
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
index ba03f82..a36091d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
@@ -33,9 +33,10 @@
    * <p>The instruction at {@code index} is traced and its target blocks are marked by using
    * {@code IRBuilder.ensureSuccessorBlock} (and {@code ensureBlockWithoutEnqueuing}).
    *
-   * @return True if the instruction closes the current block, false otherwise.
+   * @return If the instruction closes the block, the last index of the block,
+   * otherwise -1.
    */
-  boolean traceInstruction(int instructionIndex, IRBuilder builder);
+  int traceInstruction(int instructionIndex, IRBuilder builder);
 
   void closedCurrentBlockWithFallthrough(int fallthroughInstructionIndex);
   void closedCurrentBlock();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 01be50d..c3f2d08 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -166,7 +166,7 @@
 
   private boolean isInMainDexList(DexType iface) {
     ImmutableSet<DexType> list = converter.application.mainDexList;
-    return list != null && list.contains(iface);
+    return list.contains(iface);
   }
 
   // Represent a static interface method as a method of companion class.
@@ -226,7 +226,7 @@
     InterfaceProcessor processor = new InterfaceProcessor(this);
     for (DexProgramClass clazz : builder.getProgramClasses()) {
       if (shouldProcess(clazz, flavour, true)) {
-        processor.process(clazz.get().asProgramClass());
+        processor.process(clazz.asProgramClass());
       }
     }
     return processor.companionClasses;
@@ -236,7 +236,7 @@
     ClassProcessor processor = new ClassProcessor(this);
     for (DexProgramClass clazz : builder.getProgramClasses()) {
       if (shouldProcess(clazz, flavour, false)) {
-        processor.process(clazz.get());
+        processor.process(clazz);
       }
     }
     return processor.getForwardMethods();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 37bfdaa..cb518a3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -388,6 +387,10 @@
     DexClass definitionFor(DexType type) {
       return rewriter.converter.appInfo.app.definitionFor(type);
     }
+
+    DexProgramClass programDefinitionFor(DexType type) {
+      return rewriter.converter.appInfo.app.programDefinitionFor(type);
+    }
   }
 
   // Used for targeting methods referenced directly without creating accessors.
@@ -415,9 +418,7 @@
       // We already found the static method to be called, just relax its accessibility.
       assert descriptor.getAccessibility() != null;
       descriptor.getAccessibility().unsetPrivate();
-      DexClassPromise promise = definitionFor(descriptor.implHandle.asMethod().holder);
-      assert promise != null;
-      DexClass implMethodHolder = promise.get();
+      DexClass implMethodHolder = definitionFor(descriptor.implHandle.asMethod().holder);
       if (implMethodHolder.isInterface()) {
         descriptor.getAccessibility().setPublic();
       }
@@ -438,9 +439,7 @@
       // For all instantiation points for which compiler creates lambda$
       // methods, it creates these methods in the same class/interface.
       DexMethod implMethod = descriptor.implHandle.asMethod();
-      DexClassPromise promise = definitionFor(implMethod.holder);
-      assert promise != null;
-      DexClass implMethodHolder = promise.get();
+      DexClass implMethodHolder = definitionFor(implMethod.holder);
 
       DexEncodedMethod[] directMethods = implMethodHolder.directMethods;
       for (int i = 0; i < directMethods.length; i++) {
@@ -481,9 +480,8 @@
     @Override
     boolean ensureAccessibility() {
       // Create a static accessor with proper accessibility.
-      DexClassPromise promise = definitionFor(callTarget.holder);
-      assert promise != null && promise.isProgramClass();
-      DexClass accessorClass = promise.get();
+      DexProgramClass accessorClass = programDefinitionFor(callTarget.holder);
+      assert accessorClass != null;
 
       DexAccessFlags accessorFlags = new DexAccessFlags(
           Constants.ACC_SYNTHETIC | Constants.ACC_STATIC |
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 e5b8306..c280a23 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
@@ -48,6 +48,7 @@
 import com.android.tools.r8.ir.code.Switch;
 import com.android.tools.r8.ir.code.Switch.Type;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LongInterval;
 import com.google.common.base.Equivalence;
@@ -78,7 +79,7 @@
   private static final int UNKNOWN_CAN_THROW = 0;
   private static final int CAN_THROW = 1;
   private static final int CANNOT_THROW = 2;
-  private static final int MAX_FILL_ARRAY_SIZE = 4 * Constants.KILOBYTE;
+  private static final int MAX_FILL_ARRAY_SIZE = 8 * Constants.KILOBYTE;
 
   private final AppInfo appInfo;
   private final DexItemFactory dexItemFactory;
@@ -607,7 +608,7 @@
   }
 
   public void identifyReturnsArgument(
-      DexEncodedMethod method, IRCode code) {
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     if (code.getNormalExitBlock() != null) {
       Return ret = code.getNormalExitBlock().exit().asReturn();
       if (!ret.isReturnVoid()) {
@@ -616,14 +617,14 @@
           // Find the argument number.
           int index = code.collectArguments().indexOf(returnValue);
           assert index != -1;
-          method.markReturnsArgument(index);
+          feedback.methodReturnsArgument(method, index);
         }
         if (returnValue.isConstant() && returnValue.definition.isConstNumber()) {
           long value = returnValue.definition.asConstNumber().getRawValue();
-          method.markReturnsConstant(value);
+          feedback.methodReturnsConstant(method, value);
         }
         if (returnValue.isNeverNull()) {
-          method.markNeverReturnsNull();
+          feedback.methodNeverReturnsNull(method);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index f677637..f57c022 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -50,23 +50,23 @@
     this.options = options;
   }
 
-  private InliningConstraint instructionAllowedForInlining(
+  private Constraint instructionAllowedForInlining(
       DexEncodedMethod method, Instruction instruction) {
-    InliningConstraint result = instruction.inliningConstraint(appInfo, method.method.holder);
-    if ((result == InliningConstraint.NEVER) && instruction.isDebugInstruction()) {
-      return InliningConstraint.ALWAYS;
+    Constraint result = instruction.inliningConstraint(appInfo, method.method.holder);
+    if ((result == Constraint.NEVER) && instruction.isDebugInstruction()) {
+      return Constraint.ALWAYS;
     }
     return result;
   }
 
-  public InliningConstraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
+  public Constraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
     DexCode dex = method.getCode().asDexCode();
     // We have generated code for a method and we want to figure out whether the method is a
     // candidate for inlining. The code is the final IR after optimizations.
     if (dex.instructions.length > INLINING_INSTRUCTION_LIMIT) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
-    InliningConstraint result = InliningConstraint.ALWAYS;
+    Constraint result = Constraint.ALWAYS;
     ListIterator<BasicBlock> iterator = code.listIterator();
     assert iterator.hasNext();
     BasicBlock block = iterator.next();
@@ -76,9 +76,9 @@
       InstructionListIterator it = block.listIterator();
       while (it.hasNext()) {
         Instruction instruction = it.next();
-        InliningConstraint state = instructionAllowedForInlining(method, instruction);
-        if (state == InliningConstraint.NEVER) {
-          return InliningConstraint.NEVER;
+        Constraint state = instructionAllowedForInlining(method, instruction);
+        if (state == Constraint.NEVER) {
+          return Constraint.NEVER;
         }
         if (state.ordinal() < result.ordinal()) {
           result = state;
@@ -105,7 +105,7 @@
     return methodHolder.isSamePackage(targetHolder);
   }
 
-  public enum InliningConstraint {
+  public enum Constraint {
     // The ordinal values are important so please do not reorder.
     NEVER,    // Never inline this.
     PRIVATE,  // Only inline this into methods with same holder.
@@ -113,23 +113,27 @@
     ALWAYS,   // No restrictions for inlining this.
   }
 
+  public enum Reason {
+    FORCE,         // Inlinee is marked for forced inlining (bridge method or renamed constructor).
+    SINGLE_CALLER, // Inlinee has precisely one caller.
+    DUAL_CALLER,   // Inlinee has precisely two callers.
+    SIMPLE,        // Inlinee has simple code suitable for inlining.
+  }
+
   static public class InlineAction {
 
     public final DexEncodedMethod target;
     public final Invoke invoke;
-    public final boolean forceInline;
+    public final Reason reason;
 
-    public InlineAction(
-        DexEncodedMethod target, Invoke invoke, boolean forceInline) {
+    public InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
       this.target = target;
       this.invoke = invoke;
-      this.forceInline = forceInline;
+      this.reason = reason;
     }
 
-    public InlineAction(DexEncodedMethod target, Invoke invoke) {
-      this.target = target;
-      this.invoke = invoke;
-      this.forceInline = target.getOptimizationInfo().forceInline();
+    public boolean forceInline() {
+      return reason != Reason.SIMPLE;
     }
 
     public IRCode buildIR(ValueNumberGenerator generator, AppInfoWithSubtyping appInfo,
@@ -275,7 +279,7 @@
               // Back up before the invoke instruction.
               iterator.previous();
               instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.forceInline) {
+              if (instruction_allowance >= 0 || result.forceInline()) {
                 iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
               }
               // If we inlined the invoke from a bridge method, it is no longer a bridge method.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index 954f11c..7d83150 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.logging.Log;
 
 /**
@@ -103,6 +104,19 @@
     return candidate;
   }
 
+  private Reason computeInliningReason(DexEncodedMethod target) {
+    if (target.getOptimizationInfo().forceInline()) {
+      return Reason.FORCE;
+    }
+    if (callGraph.hasSingleCallSite(target)) {
+      return Reason.SINGLE_CALLER;
+    }
+    if (isDoubleInliningTarget(target)) {
+      return Reason.DUAL_CALLER;
+    }
+    return Reason.SIMPLE;
+  }
+
   public InlineAction computeForInvokeWithReceiver(InvokeMethodWithReceiver invoke) {
     boolean receiverIsNeverNull = invoke.receiverIsNeverNull();
     if (!receiverIsNeverNull) {
@@ -120,11 +134,9 @@
       return null;
     }
 
-    boolean fromBridgeMethod = target.getOptimizationInfo().forceInline();
-
     if (target == method) {
       // Bridge methods should never have recursive calls.
-      assert !fromBridgeMethod;
+      assert !target.getOptimizationInfo().forceInline();
       return null;
     }
 
@@ -154,11 +166,9 @@
       return null;
     }
 
-    boolean doubleInlineTarget = isDoubleInliningTarget(target);
+    Reason reason = computeInliningReason(target);
     // Determine if this should be inlined no matter how big it is.
-    boolean forceInline =
-        fromBridgeMethod | callGraph.hasSingleCallSite(target) | doubleInlineTarget;
-    if (!target.isInliningCandidate(method, forceInline)) {
+    if (!target.isInliningCandidate(method, reason != Reason.SIMPLE)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -175,7 +185,7 @@
     }
 
     // Attempt to inline a candidate that is only called twice.
-    if (doubleInlineTarget && (doubleInlining(target) == null)) {
+    if ((reason == Reason.DUAL_CALLER) && (doubleInlining(target) == null)) {
       if (info != null) {
         info.exclude(invoke, "target is not ready for double inlining");
       }
@@ -185,7 +195,7 @@
     if (info != null) {
       info.include(invoke.getType(), target);
     }
-    return new InlineAction(target, invoke, forceInline);
+    return new InlineAction(target, invoke, reason);
   }
 
   public InlineAction computeForInvokeVirtual(InvokeVirtual invoke) {
@@ -224,12 +234,9 @@
     if (candidate == null) {
       return null;
     }
-    boolean fromBridgeMethod = candidate.getOptimizationInfo().forceInline();
-    boolean doubleInlineTarget = isDoubleInliningTarget(candidate);
+    Reason reason = computeInliningReason(candidate);
     // Determine if this should be inlined no matter how big it is.
-    boolean forceInline =
-        fromBridgeMethod | callGraph.hasSingleCallSite(candidate) | doubleInlineTarget;
-    if (!candidate.isInliningCandidate(method, forceInline)) {
+    if (!candidate.isInliningCandidate(method, reason != Reason.SIMPLE)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -246,7 +253,7 @@
     }
 
     // Attempt to inline a candidate that is only called twice.
-    if (doubleInlineTarget && (doubleInlining(candidate) == null)) {
+    if ((reason == Reason.DUAL_CALLER) && (doubleInlining(candidate) == null)) {
       if (info != null) {
         info.exclude(invoke, "target is not ready for double inlining");
       }
@@ -256,7 +263,7 @@
     if (info != null) {
       info.include(invoke.getType(), candidate);
     }
-    return new InlineAction(candidate, invoke);
+    return new InlineAction(candidate, invoke, reason);
   }
 
   public InlineAction computeForInvokeSuper(InvokeSuper invoke) {
@@ -270,7 +277,7 @@
     if (info != null) {
       info.include(invoke.getType(), candidate);
     }
-    return new InlineAction(candidate, invoke);
+    return new InlineAction(candidate, invoke, Reason.SIMPLE);
   }
 
   public InlineAction computeForInvokePolymorpic(InvokePolymorphic invoke) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index b122d23..df5cb27 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -236,8 +236,7 @@
   private boolean isFieldRead(DexEncodedField field, boolean isStatic) {
     // Without live set information we cannot tell and assume true.
     if (liveSet == null
-        || isStatic && liveSet.staticFieldsRead.contains(field.field)
-        || !isStatic && liveSet.instanceFieldsRead.contains(field.field)
+        || liveSet.fieldsRead.contains(field.field)
         || liveSet.pinnedItems.contains(field)) {
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 42b5220..0ea5761 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -812,9 +812,9 @@
     }
 
     @Override
-    public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
+    public int traceInstruction(int instructionIndex, IRBuilder builder) {
       // There is just one block, and after the last instruction it is closed.
-      return instructionIndex == outline.templateInstructions.size();
+      return instructionIndex == outline.templateInstructions.size() ? instructionIndex : -1;
     }
 
     @Override
@@ -991,7 +991,7 @@
     }
 
     @Override
-    public String toString(ClassNameMapper naming) {
+    public String toString(DexEncodedMethod method, ClassNameMapper naming) {
       return null;
     }
   }
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 3a96ac2..9276f45 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
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.utils.CfgPrinter;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.HashMultiset;
@@ -62,6 +63,8 @@
  */
 public class LinearScanRegisterAllocator implements RegisterAllocator {
   public static final int NO_REGISTER = Integer.MIN_VALUE;
+  public static final int REGISTER_CANDIDATE_NOT_FOUND = -1;
+  public static final int MIN_CONSTANT_FREE_FOR_POSITIONS = 5;
 
   private enum ArgumentReuseMode {
     ALLOW_ARGUMENT_REUSE,
@@ -107,6 +110,8 @@
   private final IRCode code;
   // Number of registers used for arguments.
   private final int numberOfArgumentRegisters;
+  // Compiler options.
+  private final InternalOptions options;
 
   // Mapping from basic blocks to the set of values live at entry to that basic block.
   private Map<BasicBlock, Set<Value>> liveAtEntrySets = new IdentityHashMap<>();
@@ -142,8 +147,9 @@
   // register.
   private boolean hasDedicatedMoveExceptionRegister = false;
 
-  public LinearScanRegisterAllocator(IRCode code) {
+  public LinearScanRegisterAllocator(IRCode code, InternalOptions options) {
     this.code = code;
+    this.options = options;
     int argumentRegisters = 0;
     for (Instruction instruction : code.blocks.getFirst().getInstructions()) {
       if (instruction.isArgument()) {
@@ -420,6 +426,14 @@
     return getRegisterForIntervals(intervals);
   }
 
+  @Override
+  public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
+    if (value.isArgument()) {
+      return getRegisterForIntervals(value.getLiveIntervals());
+    }
+    return getRegisterForValue(value, instructionNumber);
+  }
+
   private BasicBlock[] computeLivenessInformation() {
     BasicBlock[] blocks = code.numberInstructions();
     computeLiveAtEntrySets();
@@ -557,18 +571,18 @@
         active.add(argumentInterval);
       } else {
         // Treat the argument interval as spilled which will require a load to a different
-        // register for all usages.
+        // register for all register-constrained usages.
         if (argumentInterval.getUses().size() > 1) {
           LiveIntervalsUse use = argumentInterval.firstUseWithConstraint();
           if (use != null) {
             LiveIntervals split;
-            if (argumentInterval.getUses().size() == 2) {
-              // If there is only one real use (definition plus one real use), split before
+            if (argumentInterval.numberOfUsesWithConstraint() == 1) {
+              // If there is only one register-constrained use, split before
               // that one use.
               split = argumentInterval.splitBefore(use.getPosition());
             } else {
-              // If there are multiple real users, split right after the definition to make it
-              // more likely that arguments get in usable registers from the start.
+              // If there are multiple register-constrained users, split right after the definition
+              // to make it more likely that arguments get in usable registers from the start.
               split = argumentInterval
                   .splitBefore(argumentInterval.getValue().definition.getNumber() + 1);
             }
@@ -908,17 +922,24 @@
 
     if (mode == ArgumentReuseMode.ALLOW_ARGUMENT_REUSE) {
       // The sentinel registers cannot be used and we block them.
-      freePositions.set(0, 0);
+      freePositions.set(0, 0, false);
+      if (options.debug && !code.method.accessFlags.isStatic()) {
+        // If we are generating debug information, we pin the this value register since the
+        // debugger expects to always be able to find it in the input register.
+        assert numberOfArgumentRegisters > 0;
+        assert preArgumentSentinelValue.getNextConsecutive().requiredRegisters() == 1;
+        freePositions.set(1, 0, false);
+      }
       int lastSentinelRegister = numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS - 1;
       if (lastSentinelRegister <= registerConstraint) {
-        freePositions.set(lastSentinelRegister, 0);
+        freePositions.set(lastSentinelRegister, 0, false);
       }
     } else {
       // Argument reuse is not allowed and we block all the argument registers so that
       // arguments are never free.
       for (int i = 0; i < numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS; i++) {
         if (i <= registerConstraint) {
-          freePositions.set(i, 0);
+          freePositions.set(i, 0, false);
         }
       }
     }
@@ -930,7 +951,7 @@
     if (hasDedicatedMoveExceptionRegister) {
       int moveExceptionRegister = numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS;
       if (moveExceptionRegister <= registerConstraint) {
-        freePositions.set(moveExceptionRegister, 0);
+        freePositions.set(moveExceptionRegister, 0, false);
       }
     }
 
@@ -940,7 +961,7 @@
       if (activeRegister <= registerConstraint) {
         for (int i = 0; i < intervals.requiredRegisters(); i++) {
           if (activeRegister + i <= registerConstraint) {
-            freePositions.set(activeRegister + i, 0);
+            freePositions.set(activeRegister + i, 0, intervals.isConstantNumberInterval());
           }
         }
       }
@@ -953,17 +974,18 @@
       if (inactiveRegister <= registerConstraint && unhandledInterval.overlaps(intervals)) {
         int nextOverlap = unhandledInterval.nextOverlap(intervals);
         for (int i = 0; i < intervals.requiredRegisters(); i++) {
-          if (inactiveRegister + i <= registerConstraint) {
+          int register = inactiveRegister + i;
+          if (register <= registerConstraint) {
             int unhandledStart = toInstructionPosition(unhandledInterval.getStart());
             if (nextOverlap == unhandledStart) {
               // Don't use the register for an inactive interval that is only free until the next
               // instruction. We can get into this situation when unhandledInterval starts at a
               // gap position.
-              freePositions.set(inactiveRegister + i, 0);
+              freePositions.set(register, 0, freePositions.holdsConstant(register));
             } else {
-              freePositions.set(
-                  inactiveRegister + i,
-                  Math.min(freePositions.get(inactiveRegister + i), nextOverlap));
+              if (nextOverlap < freePositions.get(register)) {
+                freePositions.set(register, nextOverlap, intervals.isConstantNumberInterval());
+              }
             }
           }
         }
@@ -978,7 +1000,8 @@
     // Get the register (pair) that is free the longest. That is the register with the largest
     // free position.
     int candidate = getLargestValidCandidate(
-        unhandledInterval, registerConstraint, needsRegisterPair, freePositions);
+        unhandledInterval, registerConstraint, needsRegisterPair, freePositions, false);
+    assert candidate != REGISTER_CANDIDATE_NOT_FOUND;
     int largestFreePosition = freePositions.get(candidate);
     if (needsRegisterPair) {
       largestFreePosition = Math.min(largestFreePosition, freePositions.get(candidate + 1));
@@ -992,7 +1015,24 @@
         // argument reuse disallowed.
         return false;
       }
-      allocateBlockedRegister(unhandledInterval);
+      // If the first use for these intervals is unconstrained, just spill this interval instead
+      // of finding another candidate to spill via allocateBlockedRegister.
+      if (!unhandledInterval.getUses().first().hasConstraint()) {
+        int nextConstrainedPosition = unhandledInterval.firstUseWithConstraint().getPosition();
+        int register;
+        // Arguments are always in the argument registers, so for arguments just use that register
+        // for the unconstrained prefix. For everything else, get a spill register.
+        if (unhandledInterval.isArgumentInterval()) {
+          register = unhandledInterval.getSplitParent().getRegister();
+        } else {
+          register = getSpillRegister(unhandledInterval);
+        }
+        LiveIntervals split = unhandledInterval.splitBefore(nextConstrainedPosition);
+        assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, register);
+        unhandled.add(split);
+      } else {
+        allocateBlockedRegister(unhandledInterval);
+      }
     } else if (largestFreePosition >= unhandledInterval.getEnd()) {
       // Free for the entire interval. Allocate the register.
       assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, candidate);
@@ -1004,8 +1044,7 @@
       }
       // The candidate is free for the beginning of an interval. We split the interval
       // and use the register for as long as we can.
-      int splitPosition = largestFreePosition;
-      LiveIntervals split = unhandledInterval.splitBefore(splitPosition);
+      LiveIntervals split = unhandledInterval.splitBefore(largestFreePosition);
       assert split != unhandledInterval;
       assignRegisterToUnhandledInterval(unhandledInterval, needsRegisterPair, candidate);
       unhandled.add(split);
@@ -1130,14 +1169,15 @@
     active.add(unhandledInterval);
   }
 
-  private int getLargestCandidate(
-      int registerConstraint, RegisterPositions freePositions, boolean needsRegisterPair) {
-    int candidate = 0;
-    int largest = freePositions.get(0);
-    if (needsRegisterPair) {
-      largest = Math.min(largest, freePositions.get(1));
-    }
-    for (int i = 1; i <= registerConstraint; i++) {
+  private int getLargestCandidate(int registerConstraint, RegisterPositions freePositions,
+      boolean needsRegisterPair, boolean restrictToConstant) {
+    int candidate = REGISTER_CANDIDATE_NOT_FOUND;
+    int largest = -1;
+
+    for (int i = 0; i <= registerConstraint; i++) {
+      if (restrictToConstant && !freePositions.holdsConstant(i)) {
+        continue;
+      }
       int freePosition = freePositions.get(i);
       if (needsRegisterPair) {
         if (i >= registerConstraint) {
@@ -1153,17 +1193,23 @@
         }
       }
     }
+
     return candidate;
   }
 
   private int getLargestValidCandidate(LiveIntervals unhandledInterval, int registerConstraint,
-      boolean needsRegisterPair, RegisterPositions freePositions) {
-    int candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair);
+      boolean needsRegisterPair, RegisterPositions freePositions, boolean restrictToConstant) {
+    int candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair,
+        restrictToConstant);
+    if (candidate == REGISTER_CANDIDATE_NOT_FOUND) {
+      return candidate;
+    }
     if (needsOverlappingLongRegisterWorkaround(unhandledInterval)) {
       while (hasOverlappingLongRegisters(unhandledInterval, candidate)) {
         // Make the overlapping register unavailable for allocation and try again.
-        freePositions.set(candidate, 0);
-        candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair);
+        freePositions.set(candidate, 0, freePositions.holdsConstant(candidate));
+        candidate = getLargestCandidate(registerConstraint, freePositions, needsRegisterPair,
+            restrictToConstant);
       }
     }
     return candidate;
@@ -1190,7 +1236,8 @@
         for (int i = 0; i < intervals.requiredRegisters(); i++) {
           if (activeRegister + i <= registerConstraint) {
             int unhandledStart = unhandledInterval.getStart();
-            usePositions.set(activeRegister + i, intervals.firstUseAfter(unhandledStart));
+            usePositions.set(activeRegister + i, intervals.firstUseAfter(unhandledStart),
+                intervals.isConstantNumberInterval());
           }
         }
       }
@@ -1202,9 +1249,11 @@
       if (inactiveRegister <= registerConstraint && intervals.overlaps(unhandledInterval)) {
         for (int i = 0; i < intervals.requiredRegisters(); i++) {
           if (inactiveRegister + i <= registerConstraint) {
-            int unhandledStart = unhandledInterval.getStart();
-            usePositions.set(inactiveRegister + i, Math.min(
-                usePositions.get(inactiveRegister + i), intervals.firstUseAfter(unhandledStart)));
+            int firstUse = intervals.firstUseAfter(unhandledInterval.getStart());
+            if (firstUse < usePositions.get(inactiveRegister + i)) {
+              usePositions.set(inactiveRegister + i, firstUse,
+                  intervals.isConstantNumberInterval());
+            }
           }
         }
       }
@@ -1213,38 +1262,46 @@
     // Disallow the reuse of argument registers by always treating them as being used
     // at instruction number 0.
     for (int i = 0; i < numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS; i++) {
-      usePositions.set(i, 0);
+      usePositions.set(i, 0, false);
     }
 
     // Disallow reuse of the move exception register if we have reserved one.
     if (hasDedicatedMoveExceptionRegister) {
-      usePositions.set(numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS, 0);
+      usePositions.set(numberOfArgumentRegisters + NUMBER_OF_SENTINEL_REGISTERS, 0, false);
     }
 
     // Treat active linked argument intervals as pinned. They cannot be given another register
     // at their uses.
-    blockLinkedRegisters(
-        active, unhandledInterval, registerConstraint, usePositions, blockedPositions);
+    blockLinkedRegisters(active, unhandledInterval, registerConstraint, usePositions,
+        blockedPositions);
 
     // Treat inactive linked argument intervals as pinned. They cannot be given another register
     // at their uses.
-    blockLinkedRegisters(
-        inactive, unhandledInterval, registerConstraint, usePositions, blockedPositions);
+    blockLinkedRegisters(inactive, unhandledInterval, registerConstraint, usePositions,
+        blockedPositions);
 
     // Get the register (pair) that has the highest use position.
     boolean needsRegisterPair = unhandledInterval.requiredRegisters() == 2;
-    int candidate = getLargestValidCandidate(
-        unhandledInterval, registerConstraint, needsRegisterPair, usePositions);
-    int largestUsePosition = usePositions.get(candidate);
-    int blockedPosition = blockedPositions.get(candidate);
-    if (needsRegisterPair) {
-      blockedPosition = Math.min(blockedPosition, blockedPositions.get(candidate + 1));
-      largestUsePosition = Math.min(largestUsePosition, usePositions.get(candidate + 1));
+
+    int candidate = getLargestValidCandidate(unhandledInterval, registerConstraint,
+        needsRegisterPair, usePositions, true);
+    if (candidate != Integer.MAX_VALUE) {
+      int nonConstCandidate = getLargestValidCandidate(unhandledInterval, registerConstraint,
+          needsRegisterPair, usePositions, false);
+      if (nonConstCandidate == Integer.MAX_VALUE || candidate == REGISTER_CANDIDATE_NOT_FOUND) {
+        candidate = nonConstCandidate;
+      } else {
+        int largestConstUsePosition = getLargestPosition(usePositions, candidate, needsRegisterPair);
+        if (largestConstUsePosition - MIN_CONSTANT_FREE_FOR_POSITIONS < unhandledInterval
+            .getStart()) {
+          // The candidate that can be rematerialized has a live range too short to use it.
+          candidate = nonConstCandidate;
+        }
+      }
     }
 
-    // TODO(ager): When selecting a spill candidate also take rematerialization into account.
-    // Prefer spilling a constant that can be rematerialized instead of spilling something that
-    // cannot be rematerialized.
+    int largestUsePosition = getLargestPosition(usePositions, candidate, needsRegisterPair);
+    int blockedPosition = getBlockedPosition(blockedPositions, candidate, needsRegisterPair);
 
     if (largestUsePosition < unhandledInterval.getFirstUse()) {
       // All active and inactive intervals are used before current. Therefore, it is best to spill
@@ -1271,6 +1328,28 @@
     }
   }
 
+  private int getLargestPosition(RegisterPositions usePositions, int register,
+      boolean needsRegisterPair) {
+    int largestUsePosition = usePositions.get(register);
+
+    if (needsRegisterPair) {
+      largestUsePosition = Math.min(largestUsePosition, usePositions.get(register + 1));
+    }
+
+    return largestUsePosition;
+  }
+
+  private int getBlockedPosition(RegisterPositions blockedPositions, int register,
+      boolean needsRegisterPair) {
+    int blockedPosition = blockedPositions.get(register);
+
+    if (needsRegisterPair) {
+      blockedPosition = Math.min(blockedPosition, blockedPositions.get(register + 1));
+    }
+
+    return blockedPosition;
+  }
+
   private void assignRegisterAndSpill(
       LiveIntervals unhandledInterval, boolean needsRegisterPair, int candidate) {
     assignRegister(unhandledInterval, candidate);
@@ -1401,7 +1480,8 @@
     assert spilled.isSpilled();
     assert spilled.getValue().isConstant();
     assert !spilled.isLinked() || spilled.isArgumentInterval();
-    int maxGapSize = 50 * INSTRUCTION_NUMBER_DELTA;
+    // Do not split range if constant is reused by one of the eleven following instruction.
+    int maxGapSize = 11 * INSTRUCTION_NUMBER_DELTA;
     if (!spilled.getUses().isEmpty()) {
       // Split at first use after the spill position and add to unhandled to get a register
       // assigned for rematerialization.
@@ -1447,15 +1527,12 @@
         if (register <= registerConstraint && other.overlaps(interval)) {
           for (int i = 0; i < other.requiredRegisters(); i++) {
             if (register + i <= registerConstraint) {
-              for (LiveIntervalsUse use : other.getUses()) {
-                if (use.getPosition() > interval.getStart()) {
-                  blockedPositions.set(
-                      register + i,
-                      Math.min(blockedPositions.get(register + i), use.getPosition()));
-                  usePositions.set(
-                      register + i,
-                      Math.min(usePositions.get(register + i), use.getPosition()));
-                }
+              int firstUse = other.firstUseAfter(interval.getStart());
+              if (firstUse < blockedPositions.get(register + i)) {
+                blockedPositions.set(register + i, firstUse, other.isConstantNumberInterval());
+                // If we start blocking registers other than linked arguments, we might need to
+                // explicitly update the use positions as well as blocked positions.
+                assert usePositions.get(register + i) <= blockedPositions.get(register + i);
               }
             }
           }
@@ -1507,7 +1584,7 @@
         // If we are processing an exception edge, we need to use the throwing instruction
         // as the instruction we are coming from.
         int fromInstruction = block.exit().getNumber();
-        boolean isCatch = block.isCatchSuccessor(successor);
+        boolean isCatch = block.hasCatchSuccessor(successor);
         if (isCatch) {
           for (Instruction instruction : block.getInstructions()) {
             if (instruction.instructionTypeCanThrow()) {
@@ -1748,7 +1825,7 @@
   private void generateArgumentMoves(Invoke invoke, InstructionListIterator insertAt) {
     // If the invoke instruction require more than 5 registers we link the inputs because they
     // need to be in consecutive registers.
-    if (invoke.requiredArgumentRegisters() > 5) {
+    if (invoke.requiredArgumentRegisters() > 5 && !argumentsAreAlreadyLinked(invoke)) {
       List<Value> arguments = invoke.arguments();
       Value previous = null;
       for (int i = 0; i < arguments.size(); i++) {
@@ -1766,6 +1843,19 @@
     }
   }
 
+  private boolean argumentsAreAlreadyLinked(Invoke invoke) {
+    Iterator<Value> it = invoke.arguments().iterator();
+    Value current = it.next();
+    while (it.hasNext()) {
+      Value next = it.next();
+      if (!current.isLinked() || current.getNextConsecutive() != next) {
+        return false;
+      }
+      current = next;
+    }
+    return true;
+  }
+
   private Value createSentinelRegisterValue() {
     return createValue(MoveType.OBJECT, null);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
index c966813..f01ed3a 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.regalloc;
 
+import static com.android.tools.r8.dex.Constants.U16BIT_MAX;
 import static com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator.NO_REGISTER;
 
 import com.android.tools.r8.dex.Constants;
@@ -29,7 +30,7 @@
   private boolean spilled = false;
 
   // Only registers up to and including the registerLimit are allowed for this interval.
-  private int registerLimit = Constants.U16BIT_MAX;
+  private int registerLimit = U16BIT_MAX;
 
   // Max register used for any of the non-spilled splits for these live intervals or for any of the
   // live intervals that this live interval is connected to by phi moves. This is used to
@@ -331,7 +332,7 @@
 
   public LiveIntervalsUse firstUseWithConstraint() {
     for (LiveIntervalsUse use : uses) {
-      if (use.getLimit() < Constants.U16BIT_MAX) {
+      if (use.hasConstraint()) {
         return use;
       }
     }
@@ -386,13 +387,14 @@
   }
 
   private void recomputeLimit() {
-    registerLimit = Constants.U16BIT_MAX;
+    registerLimit = U16BIT_MAX;
     for (LiveIntervalsUse use : uses) {
       updateRegisterConstraint(use.getLimit());
     }
   }
 
   public LiveIntervals getSplitCovering(int instructionNumber) {
+    assert getSplitParent() == this;
     // Check if this interval itself is covering the instruction.
     if (getStart() <= instructionNumber && getEnd() > instructionNumber) {
       return this;
@@ -417,6 +419,20 @@
     return null;
   }
 
+  public boolean isConstantNumberInterval() {
+    return value.definition != null && value.definition.isConstNumber();
+  }
+
+  public int numberOfUsesWithConstraint() {
+    int count = 0;
+    for (LiveIntervalsUse use : getUses()) {
+      if (use.hasConstraint()) {
+        count++;
+      }
+    }
+    return count;
+  }
+
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
@@ -468,4 +484,4 @@
       splitChild.print(printer, number + delta, number);
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
index fd37af8..a602134 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervalsUse.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.regalloc;
 
+import static com.android.tools.r8.dex.Constants.U16BIT_MAX;
+
 public class LiveIntervalsUse implements Comparable<LiveIntervalsUse> {
   private int position;
   private int limit;
@@ -41,4 +43,8 @@
     }
     return limit - o.limit;
   }
+
+  public boolean hasConstraint() {
+    return limit < U16BIT_MAX;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
index 9dba6d0..79ba8db 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
@@ -10,4 +10,5 @@
   int registersUsed();
   int getRegisterForValue(Value value, int instructionNumber);
   boolean argumentValueUsesHighRegister(Value value, int instructionNumber);
+  int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
index 87eedca..35b87fc 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.regalloc;
 
 import java.util.Arrays;
+import java.util.BitSet;
 
 /**
  * Simple mapping from a register to an int value.
@@ -16,6 +17,7 @@
   private static final int INITIAL_SIZE = 16;
   private int limit;
   private int[] backing;
+  private BitSet registerHoldsConstant;
 
   public RegisterPositions(int limit) {
     this.limit = limit;
@@ -23,13 +25,19 @@
     for (int i = 0; i < INITIAL_SIZE; i++) {
       backing[i] = Integer.MAX_VALUE;
     }
+    registerHoldsConstant = new BitSet(limit);
   }
 
-  public void set(int index, int value) {
+  public boolean holdsConstant(int index) {
+    return registerHoldsConstant.get(index);
+  }
+
+  public void set(int index, int value, boolean holdsConstant) {
     if (index >= backing.length) {
       grow(index + 1);
     }
     backing[index] = value;
+    registerHoldsConstant.set(index, holdsConstant);
   }
 
   public int get(int index) {
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
index 8c97f8f..22cf6d5 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SingleBlockSourceCode.java
@@ -124,8 +124,8 @@
   }
 
   @Override
-  public final boolean traceInstruction(int instructionIndex, IRBuilder builder) {
-    return instructionIndex == constructors.size() - 1;
+  public final int traceInstruction(int instructionIndex, IRBuilder builder) {
+    return (instructionIndex == constructors.size() - 1) ? instructionIndex : -1;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
index 7716663..3d3f9f1 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
@@ -37,7 +37,7 @@
 
   @Override
   public final String toString() {
-    return toString(null);
+    return toString(null, null);
   }
 
   private static void registerReachableDefinitionsDefault(UseRegistry registry) {
@@ -61,7 +61,7 @@
   }
 
   @Override
-  public final String toString(ClassNameMapper naming) {
+  public final String toString(DexEncodedMethod method, ClassNameMapper naming) {
     return "SynthesizedCode: " + sourceCode.toString();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index 9e9a1ad..504cc4a 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -59,10 +59,15 @@
     return canonicalizeSignature(new FieldSignature(field.name.toString(), type));
   }
 
+  /**
+   * Deobfuscate a class name.
+   *
+   * Returns the deobfuscated name if a mapping was found. Otherwise it returns the passed in name.
+   */
   public String deobfuscateClassName(String name) {
     ClassNaming classNaming = classNameMappings.get(name);
     if (classNaming == null) {
-      return null;
+      return name;
     }
     return classNaming.originalName;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index ee0614f..17da59b 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.optimize;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.Descriptor;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -98,11 +97,6 @@
     return searchClass.type;
   }
 
-  private boolean isNotFromLibrary(Descriptor item) {
-    DexClass clazz = appInfo.definitionFor(item.getHolder());
-    return clazz != null && !clazz.isLibraryClass();
-  }
-
   private DexEncodedMethod virtualLookup(DexMethod method) {
     return appInfo.lookupVirtualDefinition(method.getHolder(), method);
   }
@@ -162,12 +156,12 @@
     }
     DexProgramClass newHolder = null;
     // Recurse through supertype chain.
-    if (originalClass.superType.isSubtypeOf(targetClass.getType(), appInfo)) {
+    if (originalClass.superType.isSubtypeOf(targetClass.type, appInfo)) {
       DexClass superClass = appInfo.definitionFor(originalClass.superType);
       newHolder = findBridgeMethodHolder(superClass, targetClass, packageDescriptor);
     } else {
       for (DexType iface : originalClass.interfaces.values) {
-        if (iface.isSubtypeOf(targetClass.getType(), appInfo)) {
+        if (iface.isSubtypeOf(targetClass.type, appInfo)) {
           DexClass interfaceClass = appInfo.definitionFor(iface);
           newHolder = findBridgeMethodHolder(interfaceClass, targetClass, packageDescriptor);
         }
@@ -190,13 +184,23 @@
     for (DexField field : fields) {
       field = lense.lookupField(field, null);
       DexEncodedField target = lookup.apply(field.getHolder(), field);
-      // Rebind to the lowest library class or program class.
-      if (target != null && target.field != field) {
+      // Rebind to the lowest library class or program class. Do not rebind accesses to fields that
+      // are not public, as this might lead to access violation errors.
+      if (target != null && target.field != field && isVisibleFromOtherClasses(target)) {
         builder.map(field, validTargetFor(target.field, field, lookupTargetOnClass));
       }
     }
   }
 
+  private boolean isVisibleFromOtherClasses(DexEncodedField field) {
+    // If the field is not public, the visibility on the class can not be a further constraint.
+    if (!field.accessFlags.isPublic()) {
+      return true;
+    }
+    // If the field is public, then a non-public holder class will further constrain visibility.
+    return appInfo.definitionFor(field.field.getHolder()).accessFlags.isPublic();
+  }
+
   private static void privateMethodsCheck(DexProgramClass clazz, DexEncodedMethod method) {
     throw new Unreachable("Direct invokes should not require forwarding.");
   }
@@ -211,9 +215,9 @@
     computeMethodRebinding(appInfo.staticInvokes, appInfo::lookupStaticTarget,
         DexClass::findDirectTarget, DexProgramClass::addStaticMethod);
 
-    computeFieldRebinding(Sets.union(appInfo.staticFieldsRead, appInfo.staticFieldsWritten),
+    computeFieldRebinding(Sets.union(appInfo.staticFieldReads, appInfo.staticFieldWrites),
         appInfo::lookupStaticTarget, DexClass::findStaticTarget);
-    computeFieldRebinding(Sets.union(appInfo.instanceFieldsRead, appInfo.instanceFieldsWritten),
+    computeFieldRebinding(Sets.union(appInfo.instanceFieldReads, appInfo.instanceFieldWrites),
         appInfo::lookupInstanceTarget, DexClass::findInstanceTarget);
     return builder.build(lense, appInfo.dexItemFactory);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2dab755..abdda65 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -37,6 +37,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import java.util.ArrayDeque;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
@@ -47,6 +48,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.function.BiFunction;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -856,9 +858,8 @@
   }
 
   private Set<DexField> collectFields(Map<DexType, Set<DexField>> map) {
-    Set<DexField> set = Sets.newIdentityHashSet();
-    map.values().forEach(set::addAll);
-    return set;
+    return map.values().stream().flatMap(Collection::stream)
+        .collect(Collectors.toCollection(Sets::newIdentityHashSet));
   }
 
   Set<DexField> collectInstanceFieldsRead() {
@@ -877,6 +878,32 @@
     return Collections.unmodifiableSet(collectFields(staticFieldsWritten));
   }
 
+  private Set<DexField> collectReachedFields(Map<DexType, Set<DexField>> map,
+      Function<DexField, DexField> lookup) {
+    return map.values().stream().flatMap(set -> set.stream().map(lookup))
+        .collect(Collectors.toCollection(Sets::newIdentityHashSet));
+  }
+
+  private DexField tryLookupInstanceField(DexField field) {
+    DexEncodedField target = appInfo.lookupInstanceTarget(field.clazz, field);
+    return target == null ? field : target.field;
+  }
+
+  private DexField tryLookupStaticField(DexField field) {
+    DexEncodedField target = appInfo.lookupStaticTarget(field.clazz, field);
+    return target == null ? field : target.field;
+  }
+
+  Set<DexField> collectFieldsRead() {
+    return Sets.union(collectReachedFields(instanceFieldsRead, this::tryLookupInstanceField),
+        collectReachedFields(staticFieldsRead, this::tryLookupStaticField));
+  }
+
+  Set<DexField> collectFieldsWritten() {
+    return Sets.union(collectReachedFields(instanceFieldsWritten, this::tryLookupInstanceField),
+        collectReachedFields(staticFieldsWritten, this::tryLookupStaticField));
+  }
+
   private static class Action {
 
     final Kind kind;
@@ -981,31 +1008,29 @@
      */
     final Set<DexField> liveFields;
     /**
-     * Set of all fields which are read. Combines {@link #instanceFieldsRead} and
-     * {@link #staticFieldsRead}.
+     * Set of all fields which may be touched by a get operation. This is actual field definitions.
      */
     public final Set<DexField> fieldsRead;
     /**
-     * Set of all fields which are written. Combines {@link #instanceFieldsWritten} and
-     * {@link #staticFieldsWritten}.
+     * Set of all fields which may be touched by a put operation. This is actual field definitions.
      */
     public final Set<DexField> fieldsWritten;
     /**
-     * Set of all instance fields which are read.
+     * Set of all field ids used in instance field reads.
      */
-    public final Set<DexField> instanceFieldsRead;
+    public final Set<DexField> instanceFieldReads;
     /**
-     * Set of all instance fields which are written.
+     * Set of all field ids used in instance field writes.
      */
-    public final Set<DexField> instanceFieldsWritten;
+    public final Set<DexField> instanceFieldWrites;
     /**
-     * Set of all static fields which are read.
+     * Set of all field ids used in static static field reads.
      */
-    public final Set<DexField> staticFieldsRead;
+    public final Set<DexField> staticFieldReads;
     /**
-     * Set of all static fields which are written.
+     * Set of all field ids used in static field writes.
      */
-    public final Set<DexField> staticFieldsWritten;
+    public final Set<DexField> staticFieldWrites;
     /**
      * Set of all methods referenced in virtual invokes;
      */
@@ -1042,12 +1067,12 @@
       this.targetedMethods = toDescriptorSet(enqueuer.targetedMethods.getItems());
       this.liveMethods = toDescriptorSet(enqueuer.liveMethods.getItems());
       this.liveFields = toDescriptorSet(enqueuer.liveFields.getItems());
-      this.instanceFieldsRead = enqueuer.collectInstanceFieldsRead();
-      this.instanceFieldsWritten = enqueuer.collectInstanceFieldsWritten();
-      this.staticFieldsRead = enqueuer.collectStaticFieldsRead();
-      this.staticFieldsWritten = enqueuer.collectStaticFieldsWritten();
-      this.fieldsRead = Sets.union(staticFieldsRead, instanceFieldsRead);
-      this.fieldsWritten = Sets.union(staticFieldsWritten, instanceFieldsWritten);
+      this.instanceFieldReads = enqueuer.collectInstanceFieldsRead();
+      this.instanceFieldWrites = enqueuer.collectInstanceFieldsWritten();
+      this.staticFieldReads = enqueuer.collectStaticFieldsRead();
+      this.staticFieldWrites = enqueuer.collectStaticFieldsWritten();
+      this.fieldsRead = enqueuer.collectFieldsRead();
+      this.fieldsWritten = enqueuer.collectFieldsWritten();
       this.pinnedItems = Collections.unmodifiableSet(enqueuer.pinnedItems);
       this.virtualInvokes = joinInvokedMethods(enqueuer.virtualInvokes);
       this.superInvokes = joinInvokedMethods(enqueuer.superInvokes);
@@ -1055,8 +1080,8 @@
       this.staticInvokes = joinInvokedMethods(enqueuer.staticInvokes);
       this.noSideEffects = enqueuer.rootSet.noSideEffects;
       this.assumedValues = enqueuer.rootSet.assumedValues;
-      assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
-      assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+      assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+      assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
 
     private AppInfoWithLiveness(AppInfoWithLiveness previous, DexApplication application) {
@@ -1066,11 +1091,12 @@
       this.targetedMethods = previous.targetedMethods;
       this.liveMethods = previous.liveMethods;
       this.liveFields = previous.liveFields;
-      this.instanceFieldsRead = previous.instanceFieldsRead;
-      this.instanceFieldsWritten = previous.instanceFieldsWritten;
-      this.staticFieldsRead = previous.staticFieldsRead;
-      this.staticFieldsWritten = previous.staticFieldsWritten;
+      this.instanceFieldReads = previous.instanceFieldReads;
+      this.instanceFieldWrites = previous.instanceFieldWrites;
+      this.staticFieldReads = previous.staticFieldReads;
+      this.staticFieldWrites = previous.staticFieldWrites;
       this.fieldsRead = previous.fieldsRead;
+      // TODO(herhut): We remove fields that are only written, so maybe update this.
       this.fieldsWritten = previous.fieldsWritten;
       this.pinnedItems = previous.pinnedItems;
       this.noSideEffects = previous.noSideEffects;
@@ -1079,8 +1105,8 @@
       this.superInvokes = previous.superInvokes;
       this.directInvokes = previous.directInvokes;
       this.staticInvokes = previous.staticInvokes;
-      assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
-      assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+      assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+      assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
 
     private AppInfoWithLiveness(AppInfoWithLiveness previous, GraphLense lense) {
@@ -1090,12 +1116,12 @@
       this.targetedMethods = rewriteItems(previous.targetedMethods, lense::lookupMethod);
       this.liveMethods = rewriteItems(previous.liveMethods, lense::lookupMethod);
       this.liveFields = rewriteItems(previous.liveFields, lense::lookupField);
-      this.instanceFieldsRead = rewriteItems(previous.instanceFieldsRead, lense::lookupField);
-      this.instanceFieldsWritten = rewriteItems(previous.instanceFieldsWritten, lense::lookupField);
-      this.staticFieldsRead = rewriteItems(previous.staticFieldsRead, lense::lookupField);
-      this.staticFieldsWritten = rewriteItems(previous.staticFieldsWritten, lense::lookupField);
-      this.fieldsRead = Sets.union(staticFieldsRead, instanceFieldsRead);
-      this.fieldsWritten = Sets.union(staticFieldsWritten, instanceFieldsWritten);
+      this.instanceFieldReads = rewriteItems(previous.instanceFieldReads, lense::lookupField);
+      this.instanceFieldWrites = rewriteItems(previous.instanceFieldWrites, lense::lookupField);
+      this.staticFieldReads = rewriteItems(previous.staticFieldReads, lense::lookupField);
+      this.staticFieldWrites = rewriteItems(previous.staticFieldWrites, lense::lookupField);
+      this.fieldsRead = rewriteItems(previous.fieldsRead, lense::lookupField);
+      this.fieldsWritten = rewriteItems(previous.fieldsWritten, lense::lookupField);
       // TODO(herhut): Migrate these to Descriptors, as well.
       this.pinnedItems = previous.pinnedItems;
       this.noSideEffects = previous.noSideEffects;
@@ -1104,8 +1130,8 @@
       this.superInvokes = rewriteItems(previous.superInvokes, lense::lookupMethod);
       this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
       this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
-      assert Sets.intersection(instanceFieldsRead, staticFieldsRead).size() == 0;
-      assert Sets.intersection(instanceFieldsWritten, staticFieldsWritten).size() == 0;
+      assert Sets.intersection(instanceFieldReads, staticFieldReads).size() == 0;
+      assert Sets.intersection(instanceFieldWrites, staticFieldWrites).size() == 0;
     }
 
     private Set<DexMethod> joinInvokedMethods(Map<DexType, Set<DexMethod>> invokes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 860fc45..9fcb50a 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -41,13 +41,14 @@
           "optimizationpasses",
           "target");
   private static final List<String> ignoredOptionalSingleArgOptions = ImmutableList
-      .of("keepdirectories");
+      .of("keepdirectories", "runtype", "laststageoutput");
   private static final List<String> ignoredFlagOptions = ImmutableList
       .of("forceprocessing", "dontusemixedcaseclassnames",
           "dontpreverify", "experimentalshrinkunusedprotofields",
           "filterlibraryjarswithorginalprogramjars",
           "dontskipnonpubliclibraryclasses",
           "dontskipnonpubliclibraryclassmembers",
+          "overloadaggressively",
           "invokebasemethod");
   private static final List<String> ignoredClassDescriptorOptions = ImmutableList
       .of("isclassnamestring",
diff --git a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
index a2864e4..d03c74b 100644
--- a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
@@ -27,9 +27,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -44,6 +42,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiFunction;
+import java.util.stream.Collectors;
 
 /**
  * Merges Supertypes with a single implementation into their single subtype.
@@ -61,7 +60,7 @@
   private final GraphLense.Builder renamedMembersLense = GraphLense.builder();
   private final Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
   private final Timing timing;
-  private Set<DexMethod> invokes;
+  private Collection<DexMethod> invokes;
   private int numberOfMerges = 0;
 
   public SimpleClassMerger(DexApplication application, AppInfoWithLiveness appInfo,
@@ -81,25 +80,51 @@
         && clazz.type.getSingleSubtype() != null;
   }
 
-  private Set<DexMethod> getInvokes() {
-    if (invokes == null) {
+  private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
+      Equivalence<DexMethod> equivalence) {
+    DexClass definition = appInfo.definitionFor(method.holder);
+    if (definition != null && definition.isProgramClass()) {
+      set.add(equivalence.wrap(method));
+    }
+  }
 
-      // TODO(herhut): Ignore invokes into the library, as those can only reference library types.
-      invokes = Sets.newIdentityHashSet();
-      invokes.addAll(appInfo.directInvokes);
-      invokes.addAll(appInfo.staticInvokes);
-      invokes.addAll(appInfo.superInvokes);
-      invokes.addAll(appInfo.virtualInvokes);
-      invokes.addAll(appInfo.targetedMethods);
-      invokes.addAll(appInfo.liveMethods);
-      for (DexEncodedMethod method : Iterables
-          .filter(appInfo.pinnedItems, DexEncodedMethod.class)) {
-        invokes.add(method.method);
-      }
+  private Collection<DexMethod> getInvokes() {
+    if (invokes == null) {
+      // Collect all reachable methods that are not within a library class. Those defined on
+      // library classes are known not to have program classes in their signature.
+      // Also filter methods that only use types from library classes in their signatures. We
+      // know that those won't conflict.
+      Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
+      Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+      appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
+      invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
+          .collect(Collectors.toList());
     }
     return invokes;
   }
 
+  private boolean isProgramClass(DexType type) {
+    if (type.isArrayType()) {
+      type = type.toBaseType(appInfo.dexItemFactory);
+    }
+    if (type.isClassType()) {
+      DexClass clazz = appInfo.definitionFor(type);
+      if (clazz != null && clazz.isProgramClass()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean removeNonProgram(DexMethod dexMethod) {
+    for (DexType type : dexMethod.proto.parameters.values) {
+      if (isProgramClass(type)) {
+        return true;
+      }
+    }
+    return isProgramClass(dexMethod.proto.returnType);
+  }
+
   public GraphLense run() {
     timing.begin("merge");
     GraphLense mergingGraphLense = mergeClasses(graphLense);
@@ -426,7 +451,8 @@
         if (previous != null) {
           if (!previous.accessFlags.isBridge()) {
             if (!method.accessFlags.isBridge()) {
-              throw new CompilationError("Class merging produced invalid result.");
+              throw new CompilationError(
+                  "Class merging produced invalid result on: " + previous.toSourceString());
             } else {
               filtered.put(previous.method, previous);
             }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index f801cdd..04f6786 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -5,20 +5,16 @@
 
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Set;
 
 public class TreePruner {
@@ -52,22 +48,20 @@
   }
 
   private DexApplication.Builder removeUnused(DexApplication application) {
-    return new DexApplication.Builder(application, removeUnusedClassStructure(application));
+    return new DexApplication.Builder(application)
+        .replaceProgramClasses(getNewProgramClasses(application.classes()));
   }
 
-  private Map<DexType, DexClassPromise> removeUnusedClassStructure(DexApplication application) {
-    Map<DexType, DexClassPromise> classMap = new IdentityHashMap<>();
-    for (DexLibraryClass clazz : application.libraryClasses()) {
-      classMap.put(clazz.type, clazz);
-    }
-    for (DexProgramClass clazz : application.classes()) {
+  private List<DexProgramClass> getNewProgramClasses(List<DexProgramClass> classes) {
+    List<DexProgramClass> newClasses = new ArrayList<>();
+    for (DexProgramClass clazz : classes) {
       if (!appInfo.liveTypes.contains(clazz.type) && !options.debugKeepRules) {
         // The class is completely unused and we can remove it.
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing class: " + clazz);
         }
       } else {
-        classMap.put(clazz.type, clazz);
+        newClasses.add(clazz);
         if (!appInfo.instantiatedTypes.contains(clazz.type) && !options.debugKeepRules) {
           // The class is only needed as a type but never instantiated. Make it abstract to reflect
           // this.
@@ -89,7 +83,7 @@
         clazz.staticFields = reachableFields(clazz.staticFields());
       }
     }
-    return classMap;
+    return newClasses;
   }
 
   private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 453a250..389db15 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -7,8 +7,8 @@
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassKind;
@@ -49,14 +49,12 @@
 public class AndroidApp {
 
   public static final String DEFAULT_PROGUARD_MAP_FILE = "proguard.map";
-  public static final String DEFAULT_PROGUARD_SEEDS_FILE = "proguard.seeds";
-  public static final String DEFAULT_PACKAGE_DISTRIBUTION_FILE = "package.map";
 
   private final ImmutableList<Resource> programResources;
   private final ImmutableList<Resource> classpathResources;
   private final ImmutableList<Resource> libraryResources;
-  private final ImmutableList<ResourceProvider> classpathResourceProviders;
-  private final ImmutableList<ResourceProvider> libraryResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
   private final Resource proguardMap;
   private final Resource proguardSeeds;
   private final Resource packageDistribution;
@@ -67,8 +65,8 @@
       ImmutableList<Resource> programResources,
       ImmutableList<Resource> classpathResources,
       ImmutableList<Resource> libraryResources,
-      ImmutableList<ResourceProvider> classpathResourceProviders,
-      ImmutableList<ResourceProvider> libraryResourceProviders,
+      ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
+      ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
       Resource proguardMap,
       Resource proguardSeeds,
       Resource packageDistribution,
@@ -178,12 +176,12 @@
   }
 
   /** Get classpath resource providers. */
-  public List<ResourceProvider> getClasspathResourceProviders() {
+  public List<ClassFileResourceProvider> getClasspathResourceProviders() {
     return classpathResourceProviders;
   }
 
   /** Get library resource providers. */
-  public List<ResourceProvider> getLibraryResourceProviders() {
+  public List<ClassFileResourceProvider> getLibraryResourceProviders() {
     return libraryResourceProviders;
   }
 
@@ -290,8 +288,6 @@
         Path fileName = directory.resolve(outputMode.getFileName(dexProgramSources.get(i), i));
         Files.copy(dexProgramSources.get(i).getStream(closer), fileName, options);
       }
-      writeProguardMap(closer, directory, overwrite);
-      writeProguardSeeds(closer, directory, overwrite);
     }
   }
 
@@ -334,17 +330,6 @@
           out.closeEntry();
         }
       }
-      // Write the proguard map to the archives containing directory.
-      // TODO(zerny): How do we want to determine the output location for the extra resources?
-      writeProguardMap(closer, archive.getParent(), overwrite);
-      writeProguardSeeds(closer, archive.getParent(), overwrite);
-    }
-  }
-
-  private void writeProguardMap(Closer closer, Path parent, boolean overwrite) throws IOException {
-    InputStream input = getProguardMap(closer);
-    if (input != null) {
-      Files.copy(input, parent.resolve(DEFAULT_PROGUARD_MAP_FILE), copyOptions(overwrite));
     }
   }
 
@@ -354,12 +339,16 @@
     out.write(ByteStreams.toByteArray(input));
   }
 
-  private void writeProguardSeeds(Closer closer, Path parent, boolean overwrite)
-      throws IOException {
+  public void writeProguardSeeds(Closer closer, OutputStream out) throws IOException {
     InputStream input = getProguardSeeds(closer);
-    if (input != null) {
-      Files.copy(input, parent.resolve(DEFAULT_PROGUARD_SEEDS_FILE), copyOptions(overwrite));
-    }
+    assert input != null;
+    out.write(ByteStreams.toByteArray(input));
+  }
+
+  public void writeMainDexList(Closer closer, OutputStream out) throws IOException {
+    InputStream input = getMainDexList(closer);
+    assert input != null;
+    out.write(ByteStreams.toByteArray(input));
   }
 
   private OpenOption[] openOptions(boolean overwrite) {
@@ -383,8 +372,8 @@
     private final List<Resource> programResources = new ArrayList<>();
     private final List<Resource> classpathResources = new ArrayList<>();
     private final List<Resource> libraryResources = new ArrayList<>();
-    private final List<ResourceProvider> classpathResourceProviders = new ArrayList<>();
-    private final List<ResourceProvider> libraryResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
     private Resource proguardMap;
     private Resource proguardSeeds;
     private Resource packageDistribution;
@@ -467,7 +456,7 @@
     /**
      * Add classpath resource provider.
      */
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       classpathResourceProviders.add(provider);
       return this;
     }
@@ -492,7 +481,7 @@
     /**
      * Add library resource provider.
      */
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       libraryResourceProviders.add(provider);
       return this;
     }
@@ -588,6 +577,14 @@
     }
 
     /**
+     * Set the main-dex list data.
+     */
+    public Builder setMainDexListData(byte[] content) {
+      mainDexList = content == null ? null : Resource.fromBytes(null, content);
+      return this;
+    }
+
+    /**
      * Build final AndroidApp.
      */
     public AndroidApp build() {
@@ -644,7 +641,7 @@
                 Resource.Kind.DEX, ByteStreams.toByteArray(stream)));
           } else if (isClassFile(name)) {
             containsClassData = true;
-            String descriptor = PreloadedResourceProvider.guessTypeDescriptor(name);
+            String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
             resources(classKind).add(Resource.fromBytes(Resource.Kind.CLASSFILE,
                 ByteStreams.toByteArray(stream), Collections.singleton(descriptor)));
           }
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
new file mode 100644
index 0000000..d0f2cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -0,0 +1,151 @@
+// 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;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Represents a collection of classes. Collection can be fully loaded,
+ * lazy loaded or have preloaded classes along with lazy loaded content.
+ */
+public abstract class ClassMap<T extends DexClass> {
+  // For each type which has ever been queried stores one class loaded from
+  // resources provided by different resource providers.
+  //
+  // NOTE: all access must be synchronized on `classes`.
+  private final Map<DexType, Value<T>> classes;
+
+  // Class provider if available. In case it's `null`, all classes of
+  // the collection must be pre-populated in `classes`.
+  private final ClassProvider<T> classProvider;
+
+  ClassMap(Map<DexType, Value<T>> classes, ClassProvider<T> classProvider) {
+    this.classes = classes == null ? new IdentityHashMap<>() : classes;
+    this.classProvider = classProvider;
+    assert this.classProvider == null || this.classProvider.getClassKind() == getClassKind();
+  }
+
+  /** Resolves a class conflict by selecting a class, may generate compilation error. */
+  abstract T resolveClassConflict(T a, T b);
+
+  /** Kind of the classes supported by this collection. */
+  abstract ClassKind getClassKind();
+
+  @Override
+  public String toString() {
+    synchronized (classes) {
+      return classes.size() + " loaded, provider: " +
+          (classProvider == null ? "none" : classProvider.toString());
+    }
+  }
+
+  /** Returns a definition for a class or `null` if there is no such class in the collection. */
+  public T get(DexType type) {
+    final Value<T> value = getOrCreateValue(type);
+
+    if (!value.ready) {
+      // Load the value in a context synchronized on value instance. This way
+      // we avoid locking the whole collection during expensive resource loading
+      // and classes construction operations.
+      synchronized (value) {
+        if (!value.ready && classProvider != null) {
+          classProvider.collectClass(type, clazz -> {
+            assert clazz != null;
+            assert getClassKind().isOfKind(clazz);
+            assert !value.ready;
+
+            if (clazz.type != type) {
+              throw new CompilationError("Class content provided for type descriptor "
+                  + type.toSourceString() + " actually defines class " + clazz.type
+                  .toSourceString());
+            }
+
+            if (value.clazz == null) {
+              value.clazz = clazz;
+            } else {
+              // The class resolution *may* generate a compilation error as one of
+              // possible resolutions. In this case we leave `value` in (false, null)
+              // state so in rare case of another thread trying to get the same class
+              // before this error is propagated it will get the same conflict.
+              T oldClass = value.clazz;
+              value.clazz = null;
+              value.clazz = resolveClassConflict(oldClass, clazz);
+            }
+          });
+        }
+        value.ready = true;
+      }
+    }
+
+    assert value.ready;
+    return value.clazz;
+  }
+
+  private Value<T> getOrCreateValue(DexType type) {
+    synchronized (classes) {
+      return classes.computeIfAbsent(type, k -> new Value<>());
+    }
+  }
+
+  /** Returns currently loaded classes */
+  public List<T> collectLoadedClasses() {
+    List<T> loadedClasses = new ArrayList<>();
+    synchronized (classes) {
+      for (Value<T> value : classes.values()) {
+        // Method collectLoadedClasses() must always be called when there
+        // is no risk of concurrent loading of the classes, otherwise the
+        // behaviour of this method is undefined. Note that value mutations
+        // are NOT synchronized in `classes`, so the assertion below does
+        // not enforce this requirement, but may help detect wrong behaviour.
+        assert value.ready : "";
+        if (value.clazz != null) {
+          loadedClasses.add(value.clazz);
+        }
+      }
+    }
+    return loadedClasses;
+  }
+
+  /** Forces loading of all the classes satisfying the criteria specified. */
+  public void forceLoad(Predicate<DexType> load) {
+    if (classProvider != null) {
+      Set<DexType> loaded;
+      synchronized (classes) {
+        loaded = classes.keySet();
+      }
+      Collection<DexType> types = classProvider.collectTypes();
+      for (DexType type : types) {
+        if (load.test(type) && !loaded.contains(type)) {
+          get(type); // force-load type.
+        }
+      }
+    }
+  }
+
+  // Represents a value in the class map.
+  final static class Value<T> {
+    volatile boolean ready;
+    T clazz;
+
+    Value() {
+      ready = false;
+      clazz = null;
+    }
+
+    Value(T clazz) {
+      this.clazz = clazz;
+      this.ready = true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
new file mode 100644
index 0000000..25abdce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -0,0 +1,187 @@
+// 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;
+
+import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.io.Closer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/** Represents a provider for classes loaded from different sources. */
+public abstract class ClassProvider<T extends DexClass> {
+  private final ClassKind classKind;
+
+  ClassProvider(ClassKind classKind) {
+    this.classKind = classKind;
+  }
+
+  /** The kind of the classes created by the provider. */
+  final ClassKind getClassKind() {
+    return classKind;
+  }
+
+  /**
+   * The provider uses the callback to return all the classes that might
+   * be associated with the descriptor asked for.
+   *
+   * NOTE: the provider is not required to cache created classes and this
+   * method may create a new class instance in case it is called twice for
+   * the same type. For this reason it is recommended that the provider
+   * user only calls this method once per any given type.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract void collectClass(DexType type, Consumer<T> classConsumer);
+
+  /**
+   * Returns all the types of classes that might be produced by this provider.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract Collection<DexType> collectTypes();
+
+  /** Create class provider for java class resource provider. */
+  public static <T extends DexClass> ClassProvider<T> forClassFileResources(
+      ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+    return new ClassFileResourceReader<>(classKind, provider, reader);
+  }
+
+  /** Create class provider for preloaded classes, classes may have conflicting names. */
+  public static <T extends DexClass> ClassProvider<T> forPreloadedClasses(
+      ClassKind classKind, Collection<T> classes) {
+    ImmutableListMultimap.Builder<DexType, T> builder = ImmutableListMultimap.builder();
+    for (T clazz : classes) {
+      builder.put(clazz.type, clazz);
+    }
+    return new PreloadedClassProvider<>(classKind, builder.build());
+  }
+
+  /** Create class provider for preloaded classes. */
+  public static <T extends DexClass> ClassProvider<T> combine(
+      ClassKind classKind, List<ClassProvider<T>> providers) {
+    return new CombinedClassProvider<>(classKind, providers);
+  }
+
+  private static class ClassFileResourceReader<T extends DexClass> extends ClassProvider<T> {
+    private final ClassKind classKind;
+    private final ClassFileResourceProvider provider;
+    private final JarApplicationReader reader;
+
+    private ClassFileResourceReader(
+        ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+      super(classKind);
+      this.classKind = classKind;
+      this.provider = provider;
+      this.reader = reader;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      String descriptor = type.descriptor.toString();
+      Resource resource = provider.getResource(descriptor);
+      if (resource != null) {
+        try (Closer closer = Closer.create()) {
+          JarClassFileReader classReader =
+              new JarClassFileReader(reader, classKind.bridgeConsumer(classConsumer));
+          classReader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
+        } catch (IOException e) {
+          throw new CompilationError("Failed to load class: " + descriptor, e);
+        }
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      List<DexType> types = new ArrayList<>();
+      for (String descriptor : provider.getClassDescriptors()) {
+        types.add(reader.options.itemFactory.createType(descriptor));
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      return "class-resource-provider(" + provider.toString() + ")";
+    }
+  }
+
+  private static class PreloadedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final Multimap<DexType, T> classes;
+
+    private PreloadedClassProvider(ClassKind classKind, Multimap<DexType, T> classes) {
+      super(classKind);
+      this.classes = classes;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (T clazz : classes.get(type)) {
+        classConsumer.accept(clazz);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      return classes.keys();
+    }
+
+    @Override
+    public String toString() {
+      return "preloaded(" + classes.size() + ")";
+    }
+  }
+
+  private static class CombinedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final List<ClassProvider<T>> providers;
+
+    private CombinedClassProvider(ClassKind classKind, List<ClassProvider<T>> providers) {
+      super(classKind);
+      this.providers = providers;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (ClassProvider<T> provider : providers) {
+        provider.collectClass(type, classConsumer);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      Set<DexType> types = Sets.newIdentityHashSet();
+      for (ClassProvider<T> provider : providers) {
+        types.addAll(provider.collectTypes());
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder builder = new StringBuilder();
+      String prefix = "combined(";
+      for (ClassProvider<T> provider : providers) {
+        builder.append(prefix);
+        prefix = ", ";
+        builder.append(provider);
+      }
+      return builder.append(")").toString();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
new file mode 100644
index 0000000..ee00c39
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
@@ -0,0 +1,30 @@
+// 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;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClasspathClass;
+
+/** Represents a collection of classpath classes. */
+public class ClasspathClassCollection extends ClassMap<DexClasspathClass> {
+  public ClasspathClassCollection(ClassProvider<DexClasspathClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexClasspathClass resolveClassConflict(DexClasspathClass a, DexClasspathClass b) {
+    throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.CLASSPATH;
+  }
+
+  @Override
+  public String toString() {
+    return "classpath classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index ec430fd..eae33d1 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -88,11 +88,7 @@
         String clazz = descriptor.substring(1, descriptor.length() - 1)
             .replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
         String originalName =
-            classNameMapper == null ? null : classNameMapper.deobfuscateClassName(clazz);
-        if (originalName == null) {
-          // Class was not renamed, reconstruct the java name.
-          return clazz;
-        }
+            classNameMapper == null ? clazz : classNameMapper.deobfuscateClassName(clazz);
         return originalName;
       case '[':
         return descriptorToJavaType(descriptor.substring(1, descriptor.length()), classNameMapper)
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
new file mode 100644
index 0000000..c060ead
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
@@ -0,0 +1,76 @@
+// 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;
+
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.google.common.collect.Sets;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Lazy resource provider returning class file resources based
+ * on filesystem directory content.
+ */
+public final class DirectoryClassFileProvider implements ClassFileResourceProvider {
+  private final Path root;
+
+  private DirectoryClassFileProvider(Path root) {
+    this.root = root;
+  }
+
+  @Override
+  public Set<String> getClassDescriptors() {
+    HashSet<String> result = Sets.newHashSet();
+    collectClassDescriptors(root, result);
+    return result;
+  }
+
+  private void collectClassDescriptors(Path dir, Set<String> result) {
+    File file = dir.toFile();
+    if (file.exists()) {
+      File[] files = file.listFiles();
+      if (files != null) {
+        for (File child : files) {
+          if (child.isDirectory()) {
+            collectClassDescriptors(child.toPath(), result);
+          } else {
+            String relative = root.relativize(child.toPath()).toString();
+            if (relative.endsWith(CLASS_EXTENSION)) {
+              result.add("L" + relative.substring(
+                  0, relative.length() - CLASS_EXTENSION.length()) + ";");
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public Resource getResource(String descriptor) {
+    assert DescriptorUtils.isClassDescriptor(descriptor);
+
+    // Build expected file path based on type descriptor.
+    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
+    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
+    File file = filePath.toFile();
+
+    return (file.exists() && !file.isDirectory())
+        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
+  }
+
+  /** Create resource provider from directory path. */
+  public static ClassFileResourceProvider fromDirectory(Path dir) {
+    return new DirectoryClassFileProvider(dir.toAbsolutePath());
+  }
+
+  @Override
+  public String toString() {
+    return "directory(" + root + ")";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
deleted file mode 100644
index b003d86..0000000
--- a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.utils;
-
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Lazy resource provider based on filesystem directory content.
- *
- * NOTE: only handles classfile resources.
- */
-public final class DirectoryResourceProvider implements ResourceProvider {
-  private final Path root;
-
-  private DirectoryResourceProvider(Path root) {
-    this.root = root;
-  }
-
-  @Override
-  public Resource getResource(String descriptor) {
-    assert DescriptorUtils.isClassDescriptor(descriptor);
-
-    // Build expected file path based on type descriptor.
-    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
-    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
-    File file = filePath.toFile();
-
-    return (file.exists() && !file.isDirectory())
-        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
-  }
-
-  /** Create resource provider from directory path. */
-  public static ResourceProvider fromDirectory(Path dir) {
-    return new DirectoryResourceProvider(dir.toAbsolutePath());
-  }
-}
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 5945741..7b1cfe1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -7,12 +7,14 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardTypeMatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.function.BiFunction;
 
 public class InternalOptions {
 
@@ -35,9 +37,6 @@
   public boolean skipDebugLineNumberOpt = false;
   public boolean skipClassMerging = true;
 
-  public boolean lazyClasspathLoading = false;
-  public boolean lazyLibraryLoading = false;
-
   // Number of threads to use while processing the dex files.
   public int numberOfThreads = NOT_SPECIFIED;
   // Print smali disassembly.
@@ -46,8 +45,6 @@
   public boolean verbose = false;
   // Silencing output.
   public boolean quiet = false;
-  // Eagerly fill dex files as much as possible.
-  public boolean fillDexFiles = false;
 
   public List<String> methodsFilter = ImmutableList.of();
   public int minApiLevel = Constants.DEFAULT_ANDROID_API;
@@ -69,6 +66,8 @@
   public Path seedsFile;
   public boolean printMapping;
   public Path printMappingFile;
+  public boolean printMainDexList;
+  public Path printMainDexListFile;
   public boolean ignoreMissingClasses = false;
   public boolean skipMinification = false;
   public String packagePrefix = "";
@@ -87,11 +86,14 @@
   public List<String> obfuscationDictionary = ImmutableList.of();
 
   public ImmutableList<ProguardConfigurationRule> mainDexKeepRules = ImmutableList.of();
+  public boolean minimalMainDex;
   public ImmutableList<ProguardConfigurationRule> keepRules = ImmutableList.of();
   public ImmutableSet<ProguardTypeMatcher> dontWarnPatterns = ImmutableSet.of();
 
   public String warningInvalidParameterAnnotations = null;
 
+  public boolean overwriteOutputs; // default value is set in D/R8Command
+
   public boolean printWarnings() {
     boolean printed = false;
     if (warningInvalidParameterAnnotations != null) {
@@ -101,9 +103,13 @@
     return printed;
   }
 
+  public boolean hasMethodsFilter() {
+    return methodsFilter.size() > 0;
+  }
+
   public boolean methodMatchesFilter(DexEncodedMethod method) {
     // Not specifying a filter matches all methods.
-    if (methodsFilter.size() == 0) {
+    if (!hasMethodsFilter()) {
       return true;
     }
     // Currently the filter is simple string equality on the qualified name.
@@ -133,7 +139,7 @@
 
   public static class TestingOptions {
 
-    public boolean randomizeCallGraphLeaves = false;
+    public BiFunction<List<DexEncodedMethod>, CallGraph.Leaves, List<DexEncodedMethod>> irOrdering;
   }
 
   public static class AttributeRemovalOptions {
diff --git a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java b/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
deleted file mode 100644
index 0e71973..0000000
--- a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.utils;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.ClassKind;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexClassPromise;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.JarApplicationReader;
-import com.android.tools.r8.graph.LazyClassFileLoader;
-import com.google.common.collect.ImmutableList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a collection of classes loaded lazily from a set of lazy resource
- * providers. The collection is autonomous, it lazily collects promises but
- * does not choose the classes in cases of conflicts, delaying it until
- * the class is asked for.
- *
- * NOTE: only java class resources are allowed to be lazy loaded.
- */
-public final class LazyClassCollection {
-  // Stores promises for types which have ever been asked for before. Special value
-  // EmptyPromise.INSTANCE indicates there were no resources for this type in any of
-  // the resource providers.
-  //
-  // Promises are potentially coming from different providers and chained, but in majority
-  // of the cases there will only be one promise per type. We store promises for all
-  // the resource providers and resolve the classes at the time it is queried.
-  //
-  // Must be synchronized on `classes`.
-  private final Map<DexType, DexClassPromise> classes = new IdentityHashMap<>();
-
-  // Available lazy resource providers.
-  private final List<ResourceProvider> classpathProviders;
-  private final List<ResourceProvider> libraryProviders;
-
-  // Application reader to be used. Note that the reader may be reused in
-  // many loaders and may be used concurrently, it is considered to be
-  // thread-safe since its only state is internal options which we
-  // consider immutable after they are initialized (barring dex factory
-  // which is thread-safe).
-  private final JarApplicationReader reader;
-
-  public LazyClassCollection(JarApplicationReader reader,
-      List<ResourceProvider> classpathProviders, List<ResourceProvider> libraryProviders) {
-    this.classpathProviders = ImmutableList.copyOf(classpathProviders);
-    this.libraryProviders = ImmutableList.copyOf(libraryProviders);
-    this.reader = reader;
-  }
-
-  /**
-   * Returns a definition for a class or `null` if there is no such class.
-   * Parameter `dominator` represents the class promise that is considered
-   * to be already loaded, it may be null but if specified it may affect
-   * the conflict resolution. For example non-lazy loaded classpath class
-   * provided as `dominator` will conflict with lazy-loaded classpath classes.
-   */
-  public DexClassPromise get(DexType type, DexClassPromise dominator) {
-    DexClassPromise promise;
-
-    // Check if the promise already exists.
-    synchronized (classes) {
-      promise = classes.get(type);
-    }
-
-    if (promise == null) {
-      // Building a promise may be time consuming, we do it outside
-      // the lock so others don't have to wait.
-      promise = buildPromiseChain(type);
-
-      synchronized (classes) {
-        DexClassPromise existing = classes.get(type);
-        if (existing != null) {
-          promise = existing;
-        } else {
-          classes.put(type, promise);
-        }
-      }
-
-      assert promise != EmptyPromise.INSTANCE;
-    }
-
-    return promise == EmptyPromise.INSTANCE ? dominator : chooseClass(dominator, promise);
-  }
-
-  // Build chain of lazy promises or `null` if none of the providers
-  // provided resource for this type.
-  private DexClassPromise buildPromiseChain(DexType type) {
-    String descriptor = type.descriptor.toString();
-    DexClassPromise promise = buildPromiseChain(
-        type, descriptor, null, classpathProviders, ClassKind.CLASSPATH);
-    promise = buildPromiseChain(
-        type, descriptor, promise, libraryProviders, ClassKind.LIBRARY);
-    return promise == null ? EmptyPromise.INSTANCE : promise;
-  }
-
-  private DexClassPromise buildPromiseChain(DexType type, String descriptor,
-      DexClassPromise promise, List<ResourceProvider> providers, ClassKind classKind) {
-    for (ResourceProvider provider : providers) {
-      Resource resource = provider.getResource(descriptor);
-      if (resource == null) {
-        continue;
-      }
-
-      if (resource.kind != Resource.Kind.CLASSFILE) {
-        throw new CompilationError("Resource returned by resource provider for type " +
-            type.toSourceString() + " must be a class file resource.");
-      }
-
-      LazyClassFileLoader loader = new LazyClassFileLoader(type, resource, classKind, reader);
-      promise = (promise == null) ? loader : new DexClassPromiseChain(loader, promise);
-    }
-    return promise;
-  }
-
-  // Chooses the proper promise. Recursion is not expected to be deep.
-  private DexClassPromise chooseClass(DexClassPromise dominator, DexClassPromise candidate) {
-    DexClassPromise best = (dominator == null) ? candidate
-        : DexApplication.chooseClass(dominator, candidate, /* skipLibDups: */ true);
-    return (candidate instanceof DexClassPromiseChain)
-        ? chooseClass(best, ((DexClassPromiseChain) candidate).next) : best;
-  }
-
-  private static final class EmptyPromise implements DexClassPromise {
-    static final EmptyPromise INSTANCE = new EmptyPromise();
-
-    @Override
-    public DexType getType() {
-      throw new Unreachable();
-    }
-
-    @Override
-    public Resource.Kind getOrigin() {
-      throw new Unreachable();
-    }
-
-    @Override
-    public boolean isProgramClass() {
-      throw new Unreachable();
-    }
-
-    @Override
-    public boolean isClasspathClass() {
-      throw new Unreachable();
-    }
-
-    @Override
-    public boolean isLibraryClass() {
-      throw new Unreachable();
-    }
-
-    @Override
-    public DexClass get() {
-      throw new Unreachable();
-    }
-  }
-
-  private static final class DexClassPromiseChain implements DexClassPromise {
-    final DexClassPromise promise;
-    final DexClassPromise next;
-
-    private DexClassPromiseChain(DexClassPromise promise, DexClassPromise next) {
-      assert promise != null;
-      assert next != null;
-      this.promise = promise;
-      this.next = next;
-    }
-
-    @Override
-    public DexType getType() {
-      return promise.getType();
-    }
-
-    @Override
-    public Resource.Kind getOrigin() {
-      return promise.getOrigin();
-    }
-
-    @Override
-    public boolean isProgramClass() {
-      return promise.isProgramClass();
-    }
-
-    @Override
-    public boolean isClasspathClass() {
-      return promise.isClasspathClass();
-    }
-
-    @Override
-    public boolean isLibraryClass() {
-      return promise.isLibraryClass();
-    }
-
-    @Override
-    public DexClass get() {
-      return promise.get();
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
new file mode 100644
index 0000000..e11e660
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
@@ -0,0 +1,42 @@
+// 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;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.logging.Log;
+
+/** Represents a collection of library classes. */
+public class LibraryClassCollection extends ClassMap<DexLibraryClass> {
+  public LibraryClassCollection(ClassProvider<DexLibraryClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexLibraryClass resolveClassConflict(DexLibraryClass a, DexLibraryClass b) {
+    if (a.origin != Resource.Kind.CLASSFILE || b.origin != Resource.Kind.CLASSFILE) {
+      // We only support conflicts for classes both coming from jar files.
+      throw new CompilationError(
+          "Library type already present: " + a.type.toSourceString());
+    }
+    if (Log.ENABLED) {
+      Log.warn(DexApplication.class,
+          "Class `%s` was specified twice as a library type.", a.type.toSourceString());
+    }
+    return a;
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.LIBRARY;
+  }
+
+  @Override
+  public String toString() {
+    return "library classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
similarity index 74%
rename from src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
rename to src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index fcb89d4..07c136d 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -7,10 +7,13 @@
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
+
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -18,23 +21,25 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 import java.util.zip.ZipInputStream;
 
-/**
- * Lazy resource provider based on preloaded/prebuilt context.
- *
- * NOTE: only handles classfile resources.
- */
-public final class PreloadedResourceProvider implements ResourceProvider {
+/** Lazy Java class file resource provider based on preloaded/prebuilt context. */
+public final class PreloadedClassFileProvider implements ClassFileResourceProvider {
   private final Map<String, byte[]> content;
 
-  private PreloadedResourceProvider(Map<String, byte[]> content) {
+  private PreloadedClassFileProvider(Map<String, byte[]> content) {
     this.content = content;
   }
 
   @Override
+  public Set<String> getClassDescriptors() {
+    return Sets.newHashSet(content.keySet());
+  }
+
+  @Override
   public Resource getResource(String descriptor) {
     byte[] bytes = content.get(descriptor);
     if (bytes == null) {
@@ -44,7 +49,7 @@
   }
 
   /** Create preloaded content resource provider from archive file. */
-  public static ResourceProvider fromArchive(Path archive) throws IOException {
+  public static ClassFileResourceProvider fromArchive(Path archive) throws IOException {
     assert isArchive(archive);
     Builder builder = builder();
     try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
@@ -73,13 +78,21 @@
     assert name != null;
     assert name.endsWith(CLASS_EXTENSION) :
         "Name " + name + " must have " + CLASS_EXTENSION + " suffix";
-    String descriptor = name.substring(0, name.length() - CLASS_EXTENSION.length());
+    String fileName =
+            File.separatorChar == '/' ? name.toString() :
+                    name.toString().replace(File.separatorChar, '/');
+    String descriptor = fileName.substring(0, fileName.length() - CLASS_EXTENSION.length());
     if (descriptor.contains(".")) {
-      throw new CompilationError("Unexpected file name in the archive: " + name);
+      throw new CompilationError("Unexpected file name in the archive: " + fileName);
     }
     return 'L' + descriptor + ';';
   }
 
+  @Override
+  public String toString() {
+    return content.size() + " preloaded resources";
+  }
+
   /** Create a new empty builder. */
   public static Builder builder() {
     return new Builder();
@@ -100,9 +113,9 @@
       return this;
     }
 
-    public PreloadedResourceProvider build() {
+    public PreloadedClassFileProvider build() {
       assert content != null;
-      PreloadedResourceProvider provider = new PreloadedResourceProvider(content);
+      PreloadedClassFileProvider provider = new PreloadedClassFileProvider(content);
       content = null;
       return provider;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
new file mode 100644
index 0000000..a37eb92
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -0,0 +1,63 @@
+// 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;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Represents a collection of library classes. */
+public class ProgramClassCollection extends ClassMap<DexProgramClass> {
+  public static ProgramClassCollection create(List<DexProgramClass> classes) {
+    // We have all classes preloaded, but not necessarily without conflicts.
+    IdentityHashMap<DexType, Value<DexProgramClass>> map = new IdentityHashMap<>();
+    for (DexProgramClass clazz : classes) {
+      Value<DexProgramClass> value = map.get(clazz.type);
+      if (value == null) {
+        value = new Value<>(clazz);
+        map.put(clazz.type, value);
+      } else {
+        value.clazz = resolveClassConflictImpl(value.clazz, clazz);
+      }
+    }
+    return new ProgramClassCollection(map);
+  }
+
+  private ProgramClassCollection(IdentityHashMap<DexType, Value<DexProgramClass>> classes) {
+    super(classes, null);
+  }
+
+  @Override
+  public String toString() {
+    return "program classes: " + super.toString();
+  }
+
+  @Override
+  DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
+    return resolveClassConflictImpl(a, b);
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.PROGRAM;
+  }
+
+  private static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
+    // Currently only allow collapsing synthetic lambda classes.
+    if (a.getOrigin() == Resource.Kind.DEX
+        && b.getOrigin() == Resource.Kind.DEX
+        && a.accessFlags.isSynthetic()
+        && b.accessFlags.isSynthetic()
+        && LambdaRewriter.hasLambdaClassPrefix(a.type)
+        && LambdaRewriter.hasLambdaClassPrefix(b.type)) {
+      return a;
+    }
+    throw new CompilationError("Program type already present: " + a.type.toSourceString());
+  }
+}
diff --git a/src/test/debugTestResourcesJava8/DebugDefaultMethod.java b/src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
similarity index 77%
rename from src/test/debugTestResourcesJava8/DebugDefaultMethod.java
rename to src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
index db216b3..b407698 100644
--- a/src/test/debugTestResourcesJava8/DebugDefaultMethod.java
+++ b/src/test/debugTestResourcesJava8/DebugInterfaceMethod.java
@@ -2,13 +2,17 @@
 // 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.
 
-public class DebugDefaultMethod {
+public class DebugInterfaceMethod {
 
   interface I {
     default void doSomething(String msg) {
       String name = getClass().getName();
       System.out.println(name + ": " + msg);
     }
+
+    static void printString(String msg) {
+      System.out.println(msg);
+    }
   }
 
   static class DefaultImpl implements I {
@@ -27,9 +31,14 @@
     i.doSomething("Test");
   }
 
+  private static void testStaticMethod() {
+    I.printString("I'm a static method in interface");
+  }
+
   public static void main(String[] args) {
     testDefaultMethod(new DefaultImpl());
     testDefaultMethod(new OverrideImpl());
+    testStaticMethod();
   }
 
 }
diff --git a/src/test/debugTestResourcesJava8/DebugLambda.java b/src/test/debugTestResourcesJava8/DebugLambda.java
index c3b8695..007cd0c 100644
--- a/src/test/debugTestResourcesJava8/DebugLambda.java
+++ b/src/test/debugTestResourcesJava8/DebugLambda.java
@@ -16,8 +16,52 @@
     printInt(() -> i + j);
   }
 
-  public static void main(String[] args) {
-    DebugLambda.testLambda(5, 10);
+  private static void printInt2(I i) {
+    System.out.println(i.getInt());
   }
 
+  public static void testLambdaWithMethodReference() {
+    printInt2(DebugLambda::returnOne);
+  }
+
+  private static int returnOne() {
+    return 1;
+  }
+
+  private static void printInt3(BinaryOpInterface i, int a, int b) {
+    System.out.println(i.binaryOp(a, b));
+  }
+
+  public static void testLambdaWithArguments(int i, int j) {
+    printInt3((a, b) -> {
+      return a + b;
+    }, i, j);
+  }
+
+  interface ObjectProvider {
+    Object foo(String a, String b, String c);
+  }
+
+  private static void testLambdaWithMethodReferenceAndConversion(ObjectProvider objectProvider) {
+    System.out.println(objectProvider.foo("A", "B", "C"));
+  }
+
+  public static void main(String[] args) {
+    DebugLambda.testLambda(5, 10);
+    DebugLambda.testLambdaWithArguments(5, 10);
+    DebugLambda.testLambdaWithMethodReference();
+    DebugLambda.testLambdaWithMethodReferenceAndConversion(DebugLambda::concatObjects);
+  }
+
+  private static Object concatObjects(Object... objects) {
+    StringBuilder sb = new StringBuilder();
+    for (Object o : objects) {
+      sb.append(o.toString());
+    }
+    return sb.toString();
+  }
+
+  interface BinaryOpInterface {
+    int binaryOp(int a, int b);
+  }
 }
diff --git a/src/test/examples/memberrebinding/Test.java b/src/test/examples/memberrebinding/Memberrebinding.java
similarity index 97%
rename from src/test/examples/memberrebinding/Test.java
rename to src/test/examples/memberrebinding/Memberrebinding.java
index 62a2440..74c99fd 100644
--- a/src/test/examples/memberrebinding/Test.java
+++ b/src/test/examples/memberrebinding/Memberrebinding.java
@@ -6,7 +6,7 @@
 import memberrebinding.subpackage.PublicClass;
 import memberrebindinglib.AnIndependentInterface;
 
-public class Test {
+public class Memberrebinding {
 
   public static void main(String[] args) {
     ClassAtBottomOfChain bottomInstance = new ClassAtBottomOfChain();
diff --git a/src/test/examples/memberrebinding2/Test.java b/src/test/examples/memberrebinding2/Memberrebinding.java
similarity index 96%
rename from src/test/examples/memberrebinding2/Test.java
rename to src/test/examples/memberrebinding2/Memberrebinding.java
index d4b005e..13cde8c 100644
--- a/src/test/examples/memberrebinding2/Test.java
+++ b/src/test/examples/memberrebinding2/Memberrebinding.java
@@ -5,7 +5,7 @@
 
 import memberrebinding2.subpackage.PublicClass;
 
-public class Test {
+public class Memberrebinding {
 
   public static void main(String[] args) {
     ClassAtBottomOfChain bottomInstance = new ClassAtBottomOfChain();
diff --git a/src/test/examples/memberrebinding2/keep-rules.txt b/src/test/examples/memberrebinding2/keep-rules.txt
new file mode 100644
index 0000000..ae40c43
--- /dev/null
+++ b/src/test/examples/memberrebinding2/keep-rules.txt
@@ -0,0 +1,12 @@
+# Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Keep the application entry point. Get rid of everything that is not
+# reachable from there.
+-keep public class memberrebinding2.Memberrebinding {
+  public static void main(...);
+}
+
+# Remove once b/62048823 is fixed.
+-allowaccessmodification
\ No newline at end of file
diff --git a/src/test/examples/memberrebinding3/Test.java b/src/test/examples/memberrebinding3/Memberrebinding.java
similarity index 78%
rename from src/test/examples/memberrebinding3/Test.java
rename to src/test/examples/memberrebinding3/Memberrebinding.java
index d37f263..83d8bbb 100644
--- a/src/test/examples/memberrebinding3/Test.java
+++ b/src/test/examples/memberrebinding3/Memberrebinding.java
@@ -3,16 +3,19 @@
 // BSD-style license that can be found in the LICENSE file.
 package memberrebinding3;
 
-public class Test extends ClassAtBottomOfChain {
+public class Memberrebinding extends ClassAtBottomOfChain {
 
+  @Override
   void bottomMethod() {
 
   }
 
+  @Override
   void middleMethod() {
 
   }
 
+  @Override
   void topMethod() {
 
   }
@@ -24,6 +27,6 @@
   }
 
   public static void main(String[] args) {
-    new Test().test();
+    new Memberrebinding().test();
   }
 }
diff --git a/src/test/examplesAndroidN/memberrebinding4/Test.java b/src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
similarity index 93%
rename from src/test/examplesAndroidN/memberrebinding4/Test.java
rename to src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
index 6fc2ad5..a699537 100644
--- a/src/test/examplesAndroidN/memberrebinding4/Test.java
+++ b/src/test/examplesAndroidN/memberrebinding4/Memberrebinding.java
@@ -5,7 +5,7 @@
 
 import memberrebinding4.subpackage.PublicInterface;
 
-public class Test {
+public class Memberrebinding {
 
   static class Inner implements PublicInterface {
 
diff --git a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
index 728f769..db81f17 100644
--- a/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8IncrementalRunExamplesAndroidOTest.java
@@ -18,6 +18,8 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -80,17 +82,20 @@
         Assert.assertEquals(1, descriptors.size());
         String classDescriptor = descriptors.iterator().next();
         classDescriptor = classDescriptor.substring(1, classDescriptor.length() - 1);
-        fileToResource.put(classDescriptor + ".class", resource);
+        String classFilePath = classDescriptor + ".class";
+        if (File.separatorChar != '/') {
+          classFilePath = classFilePath.replace('/', File.separatorChar);
+        }
+        fileToResource.put(classFilePath, resource);
       }
       return fileToResource;
     }
 
     private Path makeRelative(Path testJarFile, Path classFile) {
-      classFile = classFile.toAbsolutePath();
       Path regularParent =
-          testJarFile.getParent().resolve(Paths.get("classes")).toAbsolutePath();
+          testJarFile.getParent().resolve(Paths.get("classes"));
       Path legacyParent = regularParent.resolve(Paths.get("..",
-          regularParent.getFileName().toString() + "Legacy", "classes")).toAbsolutePath();
+          regularParent.getFileName().toString() + "Legacy", "classes"));
 
       if (classFile.startsWith(regularParent)) {
         return regularParent.relativize(classFile);
@@ -102,9 +107,9 @@
     private List<String> collectClassFiles(Path testJarFile) {
       List<String> result = new ArrayList<>();
       // Collect Java 8 classes.
-      collectClassFiles(getClassesRoot(testJarFile).toFile(), result);
+      collectClassFiles(getClassesRoot(testJarFile), result);
       // Collect legacy classes.
-      collectClassFiles(getLegacyClassesRoot(testJarFile).toFile(), result);
+      collectClassFiles(getLegacyClassesRoot(testJarFile), result);
       Collections.sort(result);
       return result;
     }
@@ -121,15 +126,18 @@
       return parent.resolve(legacyPath);
     }
 
-    private void collectClassFiles(File dir, List<String> result) {
-      File[] files = dir.listFiles();
-      if (files != null) {
-        for (File file : files) {
-          if (file.isDirectory()) {
-            collectClassFiles(file, result);
-          } else {
-            result.add(file.getAbsolutePath());
+    private void collectClassFiles(Path dir, List<String> result) {
+      if (Files.exists(dir)) {
+        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
+          for (Path entry: stream) {
+            if (Files.isDirectory(entry)) {
+              collectClassFiles(entry, result);
+            } else {
+              result.add(entry.toString());
+            }
           }
+        } catch (IOException x) {
+          throw new AssertionError(x);
         }
       }
     }
diff --git a/src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
similarity index 79%
rename from src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java
rename to src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
index 7cef1a4..34cd83c 100644
--- a/src/test/java/com/android/tools/r8/D8ResourceProviderRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
@@ -4,12 +4,12 @@
 
 package com.android.tools.r8;
 
-import com.android.tools.r8.utils.DirectoryResourceProvider;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.DirectoryClassFileProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import java.io.IOException;
 import java.nio.file.Path;
 
-public class D8ResourceProviderRunExamplesAndroidOTest
+public class D8LazyRunExamplesAndroidOTest
     extends D8IncrementalRunExamplesAndroidOTest {
   class D8LazyTestRunner extends D8IncrementalTestRunner {
 
@@ -25,13 +25,13 @@
 
     private void addClasspathPath(Path location, D8Command.Builder builder) {
       builder.addClasspathResourceProvider(
-          DirectoryResourceProvider.fromDirectory(location.resolve("..")));
+          DirectoryClassFileProvider.fromDirectory(location.resolve("..")));
     }
 
     @Override
     void addLibraryReference(D8Command.Builder builder, Path location) throws IOException {
       builder.addLibraryResourceProvider(
-          PreloadedResourceProvider.fromArchive(location));
+          PreloadedClassFileProvider.fromArchive(location));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java
rename to src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
index 38acabe..42d3c47 100644
--- a/src/test/java/com/android/tools/r8/D8RegularLazyRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8NonLazyRunExamplesAndroidOTest.java
@@ -8,7 +8,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
-public class D8RegularLazyRunExamplesAndroidOTest
+public class D8NonLazyRunExamplesAndroidOTest
     extends D8IncrementalRunExamplesAndroidOTest {
   class D8LazyTestRunner extends D8IncrementalTestRunner {
 
diff --git a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
index b1934dd..f7cbee8 100644
--- a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
+++ b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
@@ -4686,8 +4686,7 @@
           // 1) t02
           // java.lang.AssertionError: java.lang.AssertionError: expected:<5> but was:<4>
 
-          .put("lang.Thread.interrupt.Thread_interrupt_A04",
-              match(D8_COMPILER, runtimes(DexVm.ART_6_0_1)))
+          .put("lang.Thread.interrupt.Thread_interrupt_A04", any())
           // Been running fine for a while then this happened on a bot:
           // 1) t01
           // java.lang.AssertionError: expected:<BLOCKED> but was:<RUNNABLE>
diff --git a/src/test/java/com/android/tools/r8/R8EntryPointTests.java b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
new file mode 100644
index 0000000..c83cea6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class R8EntryPointTests extends TestBase {
+
+  private static final String MAPPING = "mapping.txt";
+  private static final String SEEDS = "seeds.txt";
+  private static final Path INPUT_JAR =
+      Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "minification" + FileUtils.JAR_EXTENSION);
+  private static final Path PROGUARD_FLAGS =
+      Paths.get(ToolHelper.EXAMPLES_DIR, "minification",  "keep-rules.txt");
+
+  private Path testFlags;
+
+  @Before
+  public void setup() throws IOException {
+    testFlags = temp.newFile("local.flags").toPath();
+    FileUtils.writeTextFile(testFlags, ImmutableList.of(
+        "-printseeds " + SEEDS,
+        "-printmapping " + MAPPING));
+  }
+
+  @Test
+  public void testRun1Dir() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath();
+    R8.run(getCommand(out));
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun1Zip() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    R8.run(getCommand(out));
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun2Dir() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath();
+    ExecutorService executor = Executors.newWorkStealingPool(2);
+    try {
+      R8.run(getCommand(out), executor);
+    } finally {
+      executor.shutdown();
+    }
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun2Zip() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    ExecutorService executor = Executors.newWorkStealingPool(2);
+    try {
+      R8.run(getCommand(out), executor);
+    } finally {
+      executor.shutdown();
+    }
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testMainDir() throws IOException, InterruptedException {
+    Path out = temp.newFolder("outdex").toPath();
+    ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+        "--lib", ToolHelper.getDefaultAndroidJar(),
+        "--output", out.toString(),
+        "--pg-conf", PROGUARD_FLAGS.toString(),
+        "--pg-conf", testFlags.toString(),
+        INPUT_JAR.toString());
+    Assert.assertEquals(0, r8.exitCode);
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testMainZip() throws IOException, InterruptedException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+        "--lib", ToolHelper.getDefaultAndroidJar(),
+        "--output", out.toString(),
+        "--pg-conf", PROGUARD_FLAGS.toString(),
+        "--pg-conf", testFlags.toString(),
+        INPUT_JAR.toString());
+    Assert.assertEquals(0, r8.exitCode);
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  private R8Command getCommand(Path out) throws CompilationException, IOException {
+    return R8Command.builder()
+      .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()))
+      .addProgramFiles(INPUT_JAR)
+      .setOutputPath(out)
+      .addProguardConfigurationFiles(PROGUARD_FLAGS, testFlags)
+      .build();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 66844af..13a4c24 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -989,7 +989,7 @@
             builder.setMinApiLevel(minSdkVersion);
           }
           D8Output output = D8.run(builder.build());
-          output.write(Paths.get(resultPath));
+          output.write(Paths.get(resultPath), true);
           break;
         }
       case R8:
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index a264f48..5e265aa 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.JarBuilder;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -40,7 +41,6 @@
 public class R8RunExamplesTest {
 
   private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
-  private static final String DEFAULT_DEX_FILENAME = "classes.dex";
 
   // For local testing on a specific Art version(s) change this set. e.g. to
   // ImmutableSet.of(DexVm.ART_DEFAULT) or pass the option -Ddex_vm=<version> to the Java VM.
@@ -103,8 +103,8 @@
         "regress_37875803.Regress",
         "regress_37955340.Regress",
         "regress_62300145.Regress",
-        "memberrebinding2.Test",
-        "memberrebinding3.Test",
+        "memberrebinding2.Memberrebinding",
+        "memberrebinding3.Memberrebinding",
         "minification.Minification",
         "enclosingmethod.Main",
         "interfaceinlining.Main",
@@ -123,7 +123,7 @@
   private static String[] makeTest(
       DexTool tool, CompilerUnderTest compiler, CompilationMode mode, String clazz) {
     String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
-    return new String[] {pkg, tool.name(), compiler.name(), mode.name(), clazz};
+    return new String[]{pkg, tool.name(), compiler.name(), mode.name(), clazz};
   }
 
   @Rule
@@ -161,7 +161,7 @@
   }
 
   private Path getOriginalDexFile() {
-    return Paths.get(EXAMPLE_DIR, pkg, DEFAULT_DEX_FILENAME);
+    return Paths.get(EXAMPLE_DIR, pkg, FileUtils.DEFAULT_DEX_FILENAME);
   }
 
   @Rule
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 28d4872..3f31686 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -96,6 +96,15 @@
   /**
    * Compile an application with R8.
    */
+  protected AndroidApp compileWithR8(List<Class> classes, Consumer<InternalOptions> optionsConsumer)
+      throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
+    R8Command command = ToolHelper.prepareR8CommandBuilder(readClasses(classes)).build();
+    return ToolHelper.runR8(command, optionsConsumer);
+  }
+
+  /**
+   * Compile an application with R8.
+   */
   protected AndroidApp compileWithR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
       throws CompilationException, ProguardRuleParserException, ExecutionException, IOException {
     R8Command command = ToolHelper.prepareR8CommandBuilder(app).build();
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index d7fd468..e74eb2c 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -41,6 +40,7 @@
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import joptsimple.internal.Strings;
+import org.junit.Assume;
 import org.junit.rules.TemporaryFolder;
 
 public class ToolHelper {
@@ -53,6 +53,8 @@
   public static final String EXAMPLES_ANDROID_O_BUILD_DIR = BUILD_DIR + "test/examplesAndroidO/";
   public static final String SMALI_BUILD_DIR = BUILD_DIR + "test/smali/";
 
+  public static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
   private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
   private static final int DEFAULT_MIN_SDK = 14;
 
@@ -379,6 +381,10 @@
     return getPlatform().startsWith("Mac");
   }
 
+  public static boolean isWindows() {
+    return getPlatform().startsWith("Windows");
+  }
+
   public static boolean artSupported() {
     if (!isLinux() && !isMac()) {
       System.err.println("Testing on your platform is not fully supported. " +
@@ -462,9 +468,7 @@
       optionsConsumer.accept(options);
     }
     CompilationResult result = R8.runForTesting(app, options);
-    if (command.getOutputPath() != null) {
-      result.androidApp.write(command.getOutputPath(), OutputMode.Indexed);
-    }
+    R8.writeOutputs(command, options, result.androidApp);
     return result;
   }
 
@@ -519,6 +523,7 @@
   }
 
   public static ProcessResult runDX(String[] args) throws IOException {
+    Assume.assumeTrue(ToolHelper.artSupported());
     DXCommandBuilder builder = new DXCommandBuilder();
     for (String arg : args) {
       builder.appendProgramArgument(arg);
@@ -601,6 +606,7 @@
   }
 
   private static ProcessResult runArtProcess(ArtCommandBuilder builder) throws IOException {
+    Assume.assumeTrue(ToolHelper.artSupported());
     ProcessResult result = runProcess(builder.asProcessBuilder());
     if (result.exitCode != 0) {
       fail("Unexpected art failure: '" + result.stderr + "'\n" + result.stdout);
@@ -640,6 +646,7 @@
   }
 
   public static void runDex2Oat(Path file, Path outFile) throws IOException {
+    Assume.assumeTrue(ToolHelper.artSupported());
     assert Files.exists(file);
     assert ByteStreams.toByteArray(Files.newInputStream(file)).length > 0;
     List<String> command = new ArrayList<>();
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index 4131b86..81f660a 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -8,11 +8,15 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.maindexlist.MainDexListTests;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -21,6 +25,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -95,9 +100,14 @@
   }
 
   @Test
-  public void singleDexProgramIsTooLarge() throws IOException {
+  public void singleDexProgramIsTooLarge() throws IOException, ExecutionException {
+    // Generate an application that will not fit into a single dex file.
+    AndroidApp generated = MainDexListTests.generateApplication(
+        ImmutableList.of("A", "B"), Constants.ANDROID_L_API, Constants.U16BIT_MAX / 2 + 1);
+    Path applicationJar = temp.newFile("application.jar").toPath();
+    generated.write(applicationJar, OutputMode.Indexed, true);
     thrown.expect(CompilationError.class);
-    runDexer(MainDexListTests.getTwoLargeClassesAppPath().toString());
+    runDexer(applicationJar.toString());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
index 0d72a52..2cdb4d7 100644
--- a/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
+++ b/src/test/java/com/android/tools/r8/d8/DexVersionTests.java
@@ -38,17 +38,17 @@
     D8Command.Builder arithmeticBuilder = D8Command.builder().addProgramFiles(ARITHMETIC_JAR);
     D8Command.Builder arrayAccessBuilder = D8Command.builder().addProgramFiles(ARRAYACCESS_JAR);
     D8Output output = D8.run(arrayAccessBuilder.build());
-    output.write(defaultApiFolder1.getRoot().toPath());
+    output.write(defaultApiFolder1.getRoot().toPath(), true);
     output = D8.run(arrayAccessBuilder.setMinApiLevel(Constants.ANDROID_O_API).build());
-    output.write(androidOApiFolder1.getRoot().toPath());
+    output.write(androidOApiFolder1.getRoot().toPath(), true);
     output = D8.run(arrayAccessBuilder.setMinApiLevel(Constants.ANDROID_N_API).build());
-    output.write(androidNApiFolder1.getRoot().toPath());
+    output.write(androidNApiFolder1.getRoot().toPath(), true);
     output = D8.run(arithmeticBuilder.build());
-    output.write(defaultApiFolder2.getRoot().toPath());
+    output.write(defaultApiFolder2.getRoot().toPath(), true);
     output = D8.run(arithmeticBuilder.setMinApiLevel(Constants.ANDROID_O_API).build());
-    output.write(androidOApiFolder2.getRoot().toPath());
+    output.write(androidOApiFolder2.getRoot().toPath(), true);
     output = D8.run(arithmeticBuilder.setMinApiLevel(Constants.ANDROID_N_API).build());
-    output.write(androidNApiFolder2.getRoot().toPath());
+    output.write(androidNApiFolder2.getRoot().toPath(), true);
   }
 
   private Path default1() {
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 9cffe15..4cef632 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Queue;
 import java.util.TreeMap;
 import java.util.function.Consumer;
@@ -245,7 +246,10 @@
   }
 
   protected final JUnit3Wrapper.Command checkNoLocal() {
-    return inspect(t -> Assert.assertTrue(t.getLocalNames().isEmpty()));
+    return inspect(t -> {
+      List<String> localNames = t.getLocalNames();
+      Assert.assertTrue("Local variables: " + String.join(",", localNames), localNames.isEmpty());
+    });
   }
 
   protected final JUnit3Wrapper.Command checkLine(String sourceFile, int line) {
@@ -475,11 +479,13 @@
       }
 
       public void checkLocal(String localName) {
-        getVariableAt(getLocation(), localName);
+        Optional<Variable> localVar = getVariableAt(getLocation(), localName);
+        Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
       }
 
       public void checkLocal(String localName, Value expectedValue) {
-        Variable localVar = getVariableAt(getLocation(), localName);
+        Optional<Variable> localVar = getVariableAt(getLocation(), localName);
+        Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
 
         // Get value
         CommandPacket commandPacket = new CommandPacket(
@@ -488,8 +494,8 @@
         commandPacket.setNextValueAsThreadID(getThreadId());
         commandPacket.setNextValueAsFrameID(getFrameId());
         commandPacket.setNextValueAsInt(1);
-        commandPacket.setNextValueAsInt(localVar.getSlot());
-        commandPacket.setNextValueAsByte(localVar.getTag());
+        commandPacket.setNextValueAsInt(localVar.get().getSlot());
+        commandPacket.setNextValueAsByte(localVar.get().getTag());
         ReplyPacket replyPacket = getMirror().performCommand(commandPacket);
         checkReplyPacket(replyPacket, "StackFrame.GetValues command");
         int valuesCount = replyPacket.getNextValueAsInt();
@@ -594,11 +600,10 @@
       return index >= varStart && index < varEnd;
     }
 
-    private Variable getVariableAt(Location location, String localName) {
+    private Optional<Variable> getVariableAt(Location location, String localName) {
       return getVariablesAt(location).stream()
           .filter(v -> localName.equals(v.getName()))
-          .findFirst()
-          .get();
+          .findFirst();
     }
 
     private List<Variable> getVariablesAt(Location location) {
@@ -858,13 +863,16 @@
 
         @Override
         public void perform(JUnit3Wrapper testBase) {
-          Variable v = testBase.getVariableAt(testBase.debuggeeState.location, localName);
+          Optional<Variable> localVar = testBase
+              .getVariableAt(testBase.debuggeeState.location, localName);
+          Assert.assertTrue("No local '" + localName + "'", localVar.isPresent());
+
           CommandPacket setValues = new CommandPacket(StackFrameCommandSet.CommandSetID,
               StackFrameCommandSet.SetValuesCommand);
           setValues.setNextValueAsThreadID(testBase.getDebuggeeState().getThreadId());
           setValues.setNextValueAsFrameID(testBase.getDebuggeeState().getFrameId());
           setValues.setNextValueAsInt(1);
-          setValues.setNextValueAsInt(v.getSlot());
+          setValues.setNextValueAsInt(localVar.get().getSlot());
           setValues.setNextValueAsValue(newValue);
           ReplyPacket replyPacket = testBase.getMirror().performCommand(setValues);
           testBase.checkReplyPacket(replyPacket, "StackFrame.SetValues");
diff --git a/src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java b/src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
similarity index 61%
rename from src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java
rename to src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
index e0eb5ea..a7dcb60 100644
--- a/src/test/java/com/android/tools/r8/debug/DefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/debug/InterfaceMethodTest.java
@@ -9,13 +9,13 @@
 import java.util.List;
 import org.junit.Test;
 
-public class DefaultMethodTest extends DebugTestBase {
+public class InterfaceMethodTest extends DebugTestBase {
 
-  private static final String SOURCE_FILE = "DebugDefaultMethod.java";
+  private static final String SOURCE_FILE = "DebugInterfaceMethod.java";
 
   @Test
   public void testDefaultMethod() throws Throwable {
-    String debuggeeClass = "DebugDefaultMethod";
+    String debuggeeClass = "DebugInterfaceMethod";
     String parameterName = "msg";
     String localVariableName = "name";
 
@@ -23,13 +23,17 @@
     commands.add(breakpoint(debuggeeClass, "testDefaultMethod"));
     commands.add(run());
     commands.add(checkMethod(debuggeeClass, "testDefaultMethod"));
-    commands.add(checkLine(SOURCE_FILE, 27));
+    commands.add(checkLine(SOURCE_FILE, 31));
     if (!supportsDefaultMethod()) {
       // We desugared default method. This means we're going to step through an extra (forward)
       // method first.
       commands.add(stepInto());
     }
     commands.add(stepInto());
+    // TODO(shertz) we should see the local variable this even when desugaring.
+    if (supportsDefaultMethod()) {
+      commands.add(checkLocal("this"));
+    }
     commands.add(checkLocal(parameterName));
     commands.add(stepOver());
     commands.add(checkLocal(parameterName));
@@ -43,7 +47,7 @@
 
   @Test
   public void testOverrideDefaultMethod() throws Throwable {
-    String debuggeeClass = "DebugDefaultMethod";
+    String debuggeeClass = "DebugInterfaceMethod";
     String parameterName = "msg";
     String localVariableName = "newMsg";
 
@@ -52,13 +56,34 @@
     commands.add(run());
     commands.add(run() /* resume after 1st breakpoint */);
     commands.add(checkMethod(debuggeeClass, "testDefaultMethod"));
-    commands.add(checkLine(SOURCE_FILE, 27));
+    commands.add(checkLine(SOURCE_FILE, 31));
     commands.add(stepInto());
-    commands.add(checkMethod("DebugDefaultMethod$OverrideImpl", "doSomething"));
+    commands.add(checkMethod("DebugInterfaceMethod$OverrideImpl", "doSomething"));
+    commands.add(checkLocal("this"));
+    commands.add(checkLocal(parameterName));
+    commands.add(stepOver());
+    commands.add(checkLocal("this"));
+    commands.add(checkLocal(parameterName));
+    commands.add(checkLocal(localVariableName));
+    commands.add(run());
+
+    runDebugTestJava8(debuggeeClass, commands);
+  }
+
+  @Test
+  public void testStaticMethod() throws Throwable {
+    String debuggeeClass = "DebugInterfaceMethod";
+    String parameterName = "msg";
+
+    List<Command> commands = new ArrayList<>();
+    commands.add(breakpoint(debuggeeClass, "testStaticMethod"));
+    commands.add(run());
+    commands.add(checkMethod(debuggeeClass, "testStaticMethod"));
+    commands.add(checkLine(SOURCE_FILE, 35));
+    commands.add(stepInto());
     commands.add(checkLocal(parameterName));
     commands.add(stepOver());
     commands.add(checkLocal(parameterName));
-    commands.add(checkLocal(localVariableName));
     commands.add(run());
 
     runDebugTestJava8(debuggeeClass, commands);
diff --git a/src/test/java/com/android/tools/r8/debug/LambdaTest.java b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
index 52d9c70..6bdbb90 100644
--- a/src/test/java/com/android/tools/r8/debug/LambdaTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LambdaTest.java
@@ -4,17 +4,18 @@
 
 package com.android.tools.r8.debug;
 
+import org.junit.Assert;
 import org.junit.Test;
 
+// TODO(shertz) test local variables
 public class LambdaTest extends DebugTestBase {
 
   public static final String SOURCE_FILE = "DebugLambda.java";
 
   @Test
-  public void testLambdaDebugging() throws Throwable {
+  public void testLambda_ExpressionOnSameLine() throws Throwable {
     String debuggeeClass = "DebugLambda";
     String initialMethodName = "printInt";
-    // TODO(shertz) test local variables
     runDebugTestJava8(debuggeeClass,
         breakpoint(debuggeeClass, initialMethodName),
         run(),
@@ -24,4 +25,56 @@
         checkLine(SOURCE_FILE, 16),
         run());
   }
+
+  @Test
+  public void testLambda_StatementOnNewLine() throws Throwable {
+    String debuggeeClass = "DebugLambda";
+    String initialMethodName = "printInt3";
+    runDebugTestJava8(debuggeeClass,
+        breakpoint(debuggeeClass, initialMethodName),
+        run(),
+        checkMethod(debuggeeClass, initialMethodName),
+        checkLine(SOURCE_FILE, 32),
+        stepInto(INTELLIJ_FILTER),
+        checkLine(SOURCE_FILE, 37),
+        run());
+  }
+
+  @Test
+  public void testLambda_StaticMethodReference_Trivial() throws Throwable {
+    String debuggeeClass = "DebugLambda";
+    String initialMethodName = "printInt2";
+    runDebugTestJava8(debuggeeClass,
+        breakpoint(debuggeeClass, initialMethodName),
+        run(),
+        checkMethod(debuggeeClass, initialMethodName),
+        checkLine(SOURCE_FILE, 20),
+        stepInto(INTELLIJ_FILTER),
+        isRunningJava() ? LambdaTest::doNothing : stepInto(INTELLIJ_FILTER),
+        checkMethod(debuggeeClass, "returnOne"),
+        checkLine(SOURCE_FILE, 28),
+        checkNoLocal(),
+        run());
+  }
+
+  @Test
+  public void testLambda_StaticMethodReference_NonTrivial() throws Throwable {
+    String debuggeeClass = "DebugLambda";
+    String initialMethodName = "testLambdaWithMethodReferenceAndConversion";
+    runDebugTestJava8(debuggeeClass,
+        breakpoint(debuggeeClass, initialMethodName),
+        run(),
+        checkMethod(debuggeeClass, initialMethodName),
+        checkLine(SOURCE_FILE, 46),
+        stepInto(INTELLIJ_FILTER),
+        inspect(t -> Assert.assertTrue(t.getCurrentMethodName().startsWith("lambda$"))),
+        stepInto(INTELLIJ_FILTER),
+        checkMethod(debuggeeClass, "concatObjects"),
+        checkLine(SOURCE_FILE, 57),
+        checkLocal("objects"),
+        run());
+  }
+
+  private static void doNothing(JUnit3Wrapper jUnit3Wrapper) {
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
index 020809a..af726bf 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/LocalsInSwitchTestRunner.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApp;
 import org.junit.Test;
 
@@ -17,7 +18,8 @@
     AndroidApp d8App = compileWithD8(clazz);
     AndroidApp dxApp = getDxCompiledSources();
 
-    String expected = "55\n1862\n15130\n";
+    String expected = "55" + ToolHelper.LINE_SEPARATOR + "1862" + ToolHelper.LINE_SEPARATOR
+            + "15130" + ToolHelper.LINE_SEPARATOR;
     assertEquals(expected, runOnJava(clazz));
     assertEquals(expected, runOnArt(d8App, clazz.getCanonicalName()));
     assertEquals(expected, runOnArt(dxApp, clazz.getCanonicalName()));
diff --git a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
index 7370ca2..7cee4a3 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApp;
 import org.junit.Test;
 
@@ -17,7 +18,7 @@
     AndroidApp d8App = compileWithD8(clazz);
     AndroidApp dxApp = getDxCompiledSources();
 
-    String expected = "42\n42\n";
+    String expected = "42" + ToolHelper.LINE_SEPARATOR + "42" + ToolHelper.LINE_SEPARATOR;
     assertEquals(expected, runOnJava(clazz));
     assertEquals(expected, runOnArt(d8App, clazz.getCanonicalName()));
     assertEquals(expected, runOnArt(dxApp, clazz.getCanonicalName()));
diff --git a/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java b/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
index 26067c7..d1e46f3 100644
--- a/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
+++ b/src/test/java/com/android/tools/r8/dex/ExtraFileTest.java
@@ -25,7 +25,7 @@
   private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
   private static final String EXAMPLE_DEX = "memberrebinding/classes.dex";
   private static final String EXAMPLE_LIB = "memberrebindinglib/classes.dex";
-  private static final String EXAMPLE_CLASS = "memberrebinding.Test";
+  private static final String EXAMPLE_CLASS = "memberrebinding.Memberrebinding";
   private static final String EXAMPLE_PACKAGE_MAP = "memberrebinding/package.map";
   private static final String EXAMPLE_PROGUARD_MAP = "memberrebinding/proguard.map";
 
@@ -47,6 +47,7 @@
         R8Command.builder()
             .addProgramFiles(original)
             .setOutputPath(out)
+            .setMinApiLevel(Constants.ANDROID_L_API) // Allow native multidex.
             .setProguardMapFile(proguardMap)
             .setPackageDistributionFile(packageMap)
             .build();
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 80a1324..d5e66b8 100644
--- a/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/CompilationTestBase.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ArtErrorParser;
@@ -67,7 +68,11 @@
         builder.addProguardConfigurationFiles(Paths.get(pgConf));
       }
       builder.setMode(mode);
-      outputApp = ToolHelper.runR8(builder.build());
+      outputApp = ToolHelper.runR8(builder.build(),
+                  options -> {
+                    options.printSeeds = false;
+                    options.minApiLevel = Constants.ANDROID_L_API;
+                  });
     } else {
       assert compiler == CompilerUnderTest.D8;
       outputApp =
@@ -75,6 +80,7 @@
               D8Command.builder()
                   .addProgramFiles(ListUtils.map(inputs, Paths::get))
                   .setMode(mode)
+                  .setMinApiLevel(Constants.ANDROID_L_API)
                   .build());
     }
     Path out = temp.getRoot().toPath().resolve("all.zip");
diff --git a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java b/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
index 1f20559..d455841 100644
--- a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
@@ -6,8 +6,8 @@
 import static com.android.tools.r8.utils.AndroidApp.DEFAULT_PROGUARD_MAP_FILE;
 
 import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.Disassemble;
 import com.android.tools.r8.R8;
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.FileUtils;
 import java.io.IOException;
@@ -27,11 +27,8 @@
 
   static final String APP_DIR = "third_party/gmscore/v5/";
 
-  public boolean deobfuscate;
-
-  @Test
-  public void disassemble() throws IOException, ExecutionException, ProguardRuleParserException,
-      CompilationException {
+  public void testDisassemble(boolean deobfuscate, boolean smali)
+      throws IOException, ExecutionException, ProguardRuleParserException, CompilationException {
     // This test only ensures that we do not break disassembling of dex code. It does not
     // check the generated code. To make it fast, we get rid of the output.
     PrintStream originalOut = System.out;
@@ -40,7 +37,8 @@
     }));
 
     try {
-      R8Command.Builder builder = R8Command.builder();
+      Disassemble.DisassembleCommand.Builder builder = Disassemble.DisassembleCommand.builder();
+      builder.setUseSmali(smali);
       if (deobfuscate) {
         builder.setProguardMapFile(Paths.get(APP_DIR, DEFAULT_PROGUARD_MAP_FILE));
       }
@@ -54,4 +52,24 @@
       System.setOut(originalOut);
     }
   }
+
+  @Test
+  public void test1() throws  Exception {
+    testDisassemble(false, false);
+  }
+
+  @Test
+  public void test2() throws  Exception {
+    testDisassemble(false, true);
+  }
+
+  @Test
+  public void test3() throws  Exception {
+    testDisassemble(true, false);
+  }
+
+  @Test
+  public void test4() throws  Exception {
+    testDisassemble(true, true);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index 2bc8dba..74f9ef9 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -10,6 +10,9 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.OutputMode;
@@ -19,17 +22,30 @@
 import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
 
+  public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+    Collections.shuffle(methods);
+    return methods;
+  }
+
   private AndroidApp doRun()
       throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
     R8Command command =
         R8Command.builder().addProgramFiles(Paths.get(GMSCORE_V7_DIR, GMSCORE_APK)).build();
-    return ToolHelper.runR8(command, options -> options.testing.randomizeCallGraphLeaves = true);
+    return ToolHelper.runR8(
+        command, options -> {
+          // For this test just do random shuffle.
+          options.testing.irOrdering = this::shuffle;
+          // Only use one thread to process to process in the order decided by the callback.
+          options.numberOfThreads = 1;
+          options.minApiLevel = Constants.ANDROID_L_API;
+        });
   }
 
   @Test
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 b6d3dac..376ccf3 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreFixedPointTest.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import java.io.IOException;
@@ -21,14 +22,18 @@
       throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
     // First compilation.
     AndroidApp app = AndroidApp.fromProgramDirectory(Paths.get(GMSCORE_V7_DIR));
-    AndroidApp app1 = ToolHelper.runR8(app);
+    AndroidApp app1 =
+        ToolHelper.runR8(app, options -> options.minApiLevel = Constants.ANDROID_L_API);
 
     // Second compilation.
     // Add option --skip-outline-opt for second compilation. The second compilation can find
     // additional outlining opportunities as member rebinding from the first compilation can move
     // methods.
     // See b/33410508 and b/33475705.
-    AndroidApp app2 = ToolHelper.runR8(app1, options -> options.outline.enabled = false);
+    AndroidApp app2 = ToolHelper.runR8(app1, options -> {
+      options.outline.enabled = false;
+      options.minApiLevel = Constants.ANDROID_L_API;
+    });
 
     // TODO: Require that the results of the two compilations are the same.
     assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java b/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
index eaf9fe3..6acacda 100644
--- a/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/DeterministicProcessingTest.java
@@ -10,38 +10,121 @@
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
 import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class DeterministicProcessingTest extends SmaliTestBase {
+  public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+    Collections.shuffle(methods);
+    return methods;
+  }
 
+  // This test will process the code a number of times each time shuffling the order in which
+  // the methods are processed. It does not do a exhaustive probing of all permutations, so if
+  // this fails it might not fail consistently, but under all circumstances a failure does reveal
+  // non-determinism in the IR-processing.
   @Test
-  public void test()
+  public void shuffleOrderTest()
       throws IOException, ExecutionException, ProguardRuleParserException, CompilationException {
-    final int ITERATIONS = 10;
+    final int ITERATIONS = 25;
     R8Command.Builder builder =
         R8Command.builder()
             .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
             .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()));
-    List<byte[]> results = new ArrayList<>();
+    byte[] expectedDex = null;
     for (int i = 0; i < ITERATIONS; i++) {
       AndroidApp result =
           ToolHelper.runR8(
-              builder.build(), options -> options.testing.randomizeCallGraphLeaves = true);
+              builder.build(), options -> {
+                // For this test just do random shuffle.
+                options.testing.irOrdering = this::shuffle;
+                // Only use one thread to process to process in the order decided by the callback.
+                options.numberOfThreads = 1;
+              });
       List<byte[]> dex = result.writeToMemory();
       assertEquals(1, dex.size());
+      if (i == 0) {
+        assert expectedDex == null;
+        expectedDex = dex.get(0);
+      } else {
+        assertArrayEquals(expectedDex, dex.get(0));
+      }
+    }
+  }
+
+  // Global variables used by the shuffler callback.
+  int iteration = 0;
+  Class testClass = null;
+
+  public List<DexEncodedMethod> permutationsOfTwo(
+      List<DexEncodedMethod> methods, CallGraph.Leaves leaves) {
+    if (!leaves.brokeCycles()) {
+      return methods;
+    }
+    methods.sort(Comparator.comparing(DexEncodedMethod::qualifiedName));
+    assertEquals(2, methods.size());
+    String className = testClass.getTypeName();
+    // Check that we are permutating the expected methods.
+    assertEquals(className + ".a", methods.get(0).qualifiedName());
+    assertEquals(className + ".b", methods.get(1).qualifiedName());
+    if (iteration == 1) {
+      Collections.swap(methods, 0, 1);
+    }
+    return methods;
+  }
+
+  public void runTest(Class clazz, boolean inline) throws Exception {
+    final int ITERATIONS = 2;
+    testClass = clazz;
+    R8Command.Builder builder =
+        R8Command.builder()
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(clazz))
+            .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()));
+    List<byte[]> results = new ArrayList<>();
+    for (iteration = 0; iteration < ITERATIONS; iteration++) {
+      AndroidApp result =
+          ToolHelper.runR8(
+              builder.build(), options -> {
+                options.inlineAccessors = inline;
+                // Callback to determine IR processing order.
+                options.testing.irOrdering = this::permutationsOfTwo;
+                // Only use one thread to process to process in the order decided by the callback.
+                options.numberOfThreads = 1;
+              });
+      List<byte[]> dex = result.writeToMemory();
+      DexInspector x = new DexInspector(result);
+      assertEquals(1, dex.size());
       results.add(dex.get(0));
-      System.out.println(dex.get(0).length);
     }
     for (int i = 0; i < ITERATIONS - 1; i++) {
       assertArrayEquals(results.get(i), results.get(i + 1));
     }
   }
+
+  @Test
+  public void testReturnArguments() throws Exception {
+    runTest(TestClassReturnsArgument.class, false);
+  }
+
+  @Test
+  public void testReturnConstant() throws Exception {
+    runTest(TestClassReturnsConstant.class, false);
+  }
+
+  @Test
+  public void testInline() throws Exception {
+    runTest(TestClassInline.class, true);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java
new file mode 100644
index 0000000..aa50f02
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassInline.java
@@ -0,0 +1,24 @@
+// 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.ir.deterministic;
+
+public class TestClassInline {
+
+  public static boolean alwaysFalse() {
+    return false;
+  }
+
+  public static int a() {
+    return b();
+  }
+
+  public static int b() {
+    if (alwaysFalse()) {
+      a();
+      return 0;
+    }
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.java
new file mode 100644
index 0000000..ac70401
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsArgument.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.ir.deterministic;
+
+import java.util.Random;
+
+public class TestClassReturnsArgument {
+
+  public static int a(int x) {
+    return b(new Random().nextInt());
+  }
+
+  public static int b(int x) {
+    a(1);
+    return x;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.java b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.java
new file mode 100644
index 0000000..7d63f60
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/deterministic/TestClassReturnsConstant.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.ir.deterministic;
+
+import java.util.Random;
+
+public class TestClassReturnsConstant {
+
+  public static int a(int x) {
+    return b(new Random().nextInt());
+  }
+
+  public static int b(int x) {
+    a(x);
+    return 1;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index 0df0031..08e90c0 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -36,6 +36,11 @@
     public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
       return false;
     }
+
+    @Override
+    public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
+      return value.getNumber();
+    }
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
new file mode 100644
index 0000000..2db35c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/BooleanByteConfusion.java
@@ -0,0 +1,112 @@
+// 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class BooleanByteConfusion extends JasminTestBase {
+
+  private void runTest(JasminBuilder builder, String main) throws Exception {
+    String javaResult = runOnJava(builder, main);
+    String artResult = runOnArt(builder, main);
+    assertEquals(javaResult, artResult);
+    String dxArtResult = runOnArtDx(builder, main);
+    assertEquals(javaResult, dxArtResult);
+  }
+
+  @Test
+  public void booleanByteConfusion() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    // public static void foo(boolean condition) {
+    //   boolean[][] array = null;
+    //   if (condition) {
+    //     array = new boolean[][] { {true}, {false} };
+    //   }
+    //   if (array != null) {
+    //     System.out.println(array[0][0]);
+    //     System.out.println(array[1][0]);
+    //   } else {
+    //     System.out.println("null array");
+    //   }
+    // }
+    clazz.addStaticMethod("foo", ImmutableList.of("Z"), "V",
+        ".limit stack 10",
+        ".limit locals 10",
+        "  aconst_null",
+        // Compiling with new versions of javac seem to always put in this check cast, but it
+        // is not strictly required and some versions of javac seem to not emit it. The class
+        // file is still valid and runs, so we should be able to deal with it as well.
+        //
+        // "  checkcast [[Z",
+        //
+        "  astore_1",
+        "  iload_0",
+        "  ifeq Target",
+        "  iconst_2",
+        "  anewarray [Z",
+        "  dup",
+        "  iconst_0",
+        "  iconst_1",
+        "  newarray boolean",
+        "  dup",
+        "  iconst_0",
+        "  iconst_1",
+        "  bastore",
+        "  aastore",
+        "  dup",
+        "  iconst_1",
+        "  iconst_1",
+        "  newarray boolean",
+        "  dup",
+        "  iconst_0",
+        "  iconst_0",
+        "  bastore",
+        "  aastore",
+        "  astore_1",
+        "Target:",
+        "  aload_1",
+        "  ifnull PrintNull",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  aload_1",
+        "  iconst_0",
+        "  aaload",
+        "  iconst_0",
+        "  baload",
+        "  invokevirtual java/io/PrintStream/println(Z)V",
+        "  getstatic java/lang/System.out Ljava/io/PrintStream;",
+        "  aload_1",
+        "  iconst_1",
+        "  aaload",
+        "  iconst_0",
+        "  baload",
+        "  invokevirtual java/io/PrintStream.println(Z)V",
+        "  goto Return",
+        "PrintNull:",
+        "  getstatic java/lang/System.out Ljava/io/PrintStream;",
+        "  ldc \"null array\"",
+        "  invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V",
+        "Return:",
+        "  return");
+
+    // public static void main(String args[]) {
+    //   foo(true);
+    //   foo(false);
+    // }
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 10",
+        "  iconst_1",
+        "  invokestatic Test/foo(Z)V",
+        "  iconst_0",
+        "  invokestatic Test/foo(Z)V",
+        "  return");
+
+    runTest(builder, clazz.name);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java
new file mode 100644
index 0000000..1812fe1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidClassNames.java
@@ -0,0 +1,80 @@
+// 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.fail;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.CompilationError;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidClassNames extends JasminTestBase {
+
+  public boolean runsOnJVM;
+  public String name;
+
+  public InvalidClassNames(String name, boolean runsOnJVM) {
+    this.name = name;
+    this.runsOnJVM = runsOnJVM;
+  }
+
+  private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+    if (runsOnJVM) {
+      String javaResult = runOnJava(builder, main);
+      assertEquals(expected, javaResult);
+    }
+    String artResult = null;
+    try {
+      artResult = runOnArt(builder, main);
+    } catch (CompilationError t) {
+      // Ignore.
+    } catch (Throwable t) {
+      t.printStackTrace(System.out);
+      fail("Invalid dex class names should be compilation errors.");
+    }
+    assert artResult == null : "Invalid dex class names should be rejected.";
+  }
+
+  @Parameters
+  public static Collection<Object[]> data() {
+    return Arrays.asList(new Object[][] {
+        { new String(new int[] { 0x00a0 }, 0, 1), true },
+        { new String(new int[] { 0x2000 }, 0, 1), !ToolHelper.isWindows() },
+        { new String(new int[] { 0x200f }, 0, 1), !ToolHelper.isWindows() },
+        { new String(new int[] { 0x2028 }, 0, 1), !ToolHelper.isWindows() },
+        { new String(new int[] { 0x202f }, 0, 1), !ToolHelper.isWindows() },
+        { new String(new int[] { 0xd800 }, 0, 1), false },
+        { new String(new int[] { 0xdfff }, 0, 1), false },
+        { new String(new int[] { 0xfff0 }, 0, 1), !ToolHelper.isWindows() },
+        { new String(new int[] { 0xffff }, 0, 1), !ToolHelper.isWindows() },
+        { "a/b/c/a/D/", true },
+        { "a<b", !ToolHelper.isWindows() },
+        { "a>b", !ToolHelper.isWindows() },
+        { "<a>b", !ToolHelper.isWindows() },
+        { "<a>", !ToolHelper.isWindows() }
+    });
+  }
+
+  @Test
+  public void invalidClassName() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass(name);
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc \"MAIN\"",
+        "  invokevirtual java/io/PrintStream.print(Ljava/lang/String;)V",
+        "  return");
+
+    runTest(builder, clazz.name, "MAIN");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java b/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
index 984e40e..155438f 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ToolHelper;
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
 
@@ -66,7 +67,7 @@
         "  invokestatic Test/foo(I)V",
         "  return");
 
-    String expected = "42\n0\n";
+    String expected = "42" + ToolHelper.LINE_SEPARATOR + "0" + ToolHelper.LINE_SEPARATOR;
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
     String artResult = runOnArtD8(builder, clazz.name);
@@ -115,7 +116,7 @@
         "  invokestatic Test/foo(II)V",
         "  return");
 
-    String expected = "42\n";
+    String expected = "42" + ToolHelper.LINE_SEPARATOR;
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
     String artResult = runOnArtD8(builder, clazz.name);
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
new file mode 100644
index 0000000..791ecca
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidFieldNames.java
@@ -0,0 +1,78 @@
+// 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+
+import com.android.tools.r8.errors.CompilationError;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidFieldNames extends JasminTestBase {
+
+  public boolean runsOnJVM;
+  public String name;
+
+  public InvalidFieldNames(String name, boolean runsOnJVM) {
+    this.name = name;
+    this.runsOnJVM = runsOnJVM;
+  }
+
+  private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+    if (runsOnJVM) {
+      String javaResult = runOnJava(builder, main);
+      assertEquals(expected, javaResult);
+    }
+    String artResult = null;
+    try {
+      artResult = runOnArt(builder, main);
+    } catch (CompilationError t) {
+      // Ignore.
+    }
+    assert artResult == null : "Invalid dex class names should be rejected.";
+  }
+
+  @Parameters
+  public static Collection<Object[]> data() {
+    return Arrays.asList(new Object[][] {
+        { new String(new int[] { 0x00a0 }, 0, 1), true },
+        { new String(new int[] { 0x2000 }, 0, 1), true },
+        { new String(new int[] { 0x200f }, 0, 1), true },
+        { new String(new int[] { 0x2028 }, 0, 1), true },
+        { new String(new int[] { 0x202f }, 0, 1), true },
+        { new String(new int[] { 0xd800 }, 0, 1), true },
+        { new String(new int[] { 0xdfff }, 0, 1), true },
+        { new String(new int[] { 0xfff0 }, 0, 1), true },
+        { new String(new int[] { 0xffff }, 0, 1), true },
+        { "a/b", false },
+        { "<a", false },
+        { "a>", true },
+        { "a<b>", true },
+        { "<a>b", true }
+    });
+  }
+
+  @Test
+  public void invalidFieldNames() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    clazz.addStaticField(name, "I", "42");
+
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  getstatic Test/" + name + " I",
+        "  invokevirtual java/io/PrintStream.print(I)V",
+        "  return");
+
+    runTest(builder, clazz.name, "42");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java b/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java
new file mode 100644
index 0000000..f8b7615
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidMethodNames.java
@@ -0,0 +1,86 @@
+// 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.jasmin;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.fail;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvalidMethodNames extends JasminTestBase {
+
+  public boolean runsOnJVM;
+  public String name;
+
+  public InvalidMethodNames(String name, boolean runsOnJVM) {
+    this.name = name;
+    this.runsOnJVM = runsOnJVM;
+  }
+
+  private void runTest(JasminBuilder builder, String main, String expected) throws Exception {
+    if (runsOnJVM) {
+      String javaResult = runOnJava(builder, main);
+      assertEquals(expected, javaResult);
+    }
+    String artResult = null;
+    try {
+      artResult = runOnArt(builder, main);
+    } catch (CompilationError t) {
+      // Ignore.
+    } catch (Throwable t) {
+      t.printStackTrace(System.out);
+      fail("Invalid dex class names should be compilation errors.");
+    }
+    assert artResult == null : "Invalid dex class names should be rejected.";
+  }
+
+  @Parameters
+  public static Collection<Object[]> data() {
+    return Arrays.asList(new Object[][] {
+        { new String(new int[] { 0x00a0 }, 0, 1), true },
+        { new String(new int[] { 0x2000 }, 0, 1), true },
+        { new String(new int[] { 0x200f }, 0, 1), true },
+        { new String(new int[] { 0x2028 }, 0, 1), true },
+        { new String(new int[] { 0x202f }, 0, 1), true },
+        { new String(new int[] { 0xd800 }, 0, 1), true },
+        { new String(new int[] { 0xdfff }, 0, 1), true },
+        { new String(new int[] { 0xfff0 }, 0, 1), true },
+        { new String(new int[] { 0xffff }, 0, 1), true },
+        { "a/b", false },
+        { "<a", false },
+        { "a>", true },
+        { "<a>", false }
+    });
+  }
+
+  @Test
+  public void invalidMethodName() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    clazz.addStaticMethod(name, ImmutableList.of(), "V",
+        ".limit stack 2",
+        ".limit locals 0",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc \"CALLED\"",
+        "  invokevirtual java/io/PrintStream.print(Ljava/lang/String;)V",
+        "  return");
+
+    clazz.addMainMethod(
+        ".limit stack 0",
+        ".limit locals 1",
+        "  invokestatic Test/" + name + "()V",
+        "  return");
+
+    runTest(builder, clazz.name, "CALLED");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java b/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
index 197b4d1..c74464f 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvokeSpecialTests.java
@@ -5,7 +5,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ToolHelper;
 import com.google.common.collect.ImmutableList;
+import org.junit.AssumptionViolatedException;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -72,7 +74,9 @@
 
     // TODO(zerny): Should we fail early on the above code? Art fails with a verification error
     // because Test.foo is expected to be in the direct method table.
-    thrown.expect(AssertionError.class);
+    if (ToolHelper.artSupported()) {
+      thrown.expect(AssertionError.class);
+    }
     String artResult = runOnArt(builder, clazz.name);
     assertEquals(expected, artResult);
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index e7511e5..7f65c60 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -25,6 +26,7 @@
   public static class ClassBuilder {
     public final String name;
     private final List<String> methods = new ArrayList<>();
+    private final List<String> fields = new ArrayList<>();
     private boolean makeInit = false;
 
     public ClassBuilder(String name) {
@@ -85,6 +87,12 @@
       return new MethodSignature(name, returnJavaType, argumentJavaTypes);
     }
 
+    public FieldSignature addStaticField(String name, String type, String value) {
+      fields.add(
+          ".field public static " + name + " " + type + (value != null ? (" = " + value) : ""));
+      return new FieldSignature(name, type);
+    }
+
     @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
@@ -101,6 +109,9 @@
             .append("  return\n")
             .append(".end method\n");
       }
+      for (String field : fields) {
+        builder.append(field).append("\n");
+      }
       for (String method : methods) {
         builder.append(method).append("\n");
       }
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 615e289..6ceb912 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -24,6 +24,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.StringReader;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
@@ -42,7 +43,9 @@
     for (ClassBuilder clazz : builder.getClasses()) {
       ClassFile file = new ClassFile();
       file.readJasmin(new StringReader(clazz.toString()), clazz.name, true);
-      file.write(new FileOutputStream(out.toPath().resolve(clazz.name + ".class").toFile()));
+      Path path = out.toPath().resolve(clazz.name + ".class");
+      Files.createDirectories(path.getParent());
+      file.write(new FileOutputStream(path.toFile()));
     }
     return ToolHelper.runJava(ImmutableList.of(out.getPath()), main);
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
index 34ddd69..bd060b5 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
@@ -308,7 +309,8 @@
         "  invokestatic Test/foo()V",
         "  return");
 
-    runTest(builder, clazz.name, "Got zero\nGot non-zero\n");
+    runTest(builder, clazz.name, "Got zero" + ToolHelper.LINE_SEPARATOR + "Got non-zero"
+            + ToolHelper.LINE_SEPARATOR);
   }
 
   @Test
@@ -351,7 +353,9 @@
         "  invokestatic Test/foo()V",
         "  return");
 
-    runTest(builder, clazz.name, "Got zero\nGot non-zero, calling nested\nIn nested subroutine\n");
+    runTest(builder, clazz.name, "Got zero" + ToolHelper.LINE_SEPARATOR
+            + "Got non-zero, calling nested" + ToolHelper.LINE_SEPARATOR + "In nested subroutine"
+            + ToolHelper.LINE_SEPARATOR);
   }
 
   @Test
@@ -527,7 +531,8 @@
         "  invokestatic Test/foo()V",
         "  return");
 
-    runTest(builder, clazz.name, "Divided by zero\nDivided by non-zero\n");
+    runTest(builder, clazz.name, "Divided by zero" + ToolHelper.LINE_SEPARATOR
+            + "Divided by non-zero" + ToolHelper.LINE_SEPARATOR);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java b/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
index 92f99f8..a24ffd2 100644
--- a/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/TryCatchStateTests.java
@@ -5,6 +5,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ToolHelper;
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
 
@@ -49,7 +50,7 @@
         "  invokevirtual java/io/PrintStream/print(I)V",
         "  return");
 
-    String expected = "0\n6";
+    String expected = "0" + ToolHelper.LINE_SEPARATOR + "6";
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
     String artResult = runOnArt(builder, clazz.name);
@@ -97,7 +98,7 @@
         "  invokevirtual java/io/PrintStream/print(I)V",
         "  return");
 
-    String expected = "12\n21";
+    String expected = "12" + ToolHelper.LINE_SEPARATOR + "21";
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
     String artResult = runOnArt(builder, clazz.name);
@@ -148,7 +149,7 @@
         "  invokevirtual java/io/PrintStream/print(I)V",
         "  return");
 
-    String expected = "12\n21";
+    String expected = "12" + ToolHelper.LINE_SEPARATOR + "21";
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
     String artResult = runOnArt(builder, clazz.name);
diff --git a/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
new file mode 100644
index 0000000..b70e73f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/HelloWorldMain.java
@@ -0,0 +1,12 @@
+// 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.maindexlist;
+
+public class HelloWorldMain {
+
+  public static void main(String[] args) {
+    System.out.println("Hello, world!");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
new file mode 100644
index 0000000..f8ca2a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -0,0 +1,48 @@
+// 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.maindexlist;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+
+public class MainDexListOutputTest extends TestBase {
+  @Test
+  public void testNoMainDex() throws Exception {
+    Path mainDexList = temp.newFile().toPath();
+    compileWithR8(ImmutableList.of(HelloWorldMain.class),
+        options -> {
+          options.printMainDexList = true;
+          options.printMainDexListFile = mainDexList;
+        });
+    // Empty main dex list.
+    assertEquals(0, FileUtils.readTextFile(mainDexList).size());
+  }
+
+  @Test
+  public void testWithMainDex() throws Exception {
+    Path mainDexRules = writeTextToTempFile(keepMainProguardConfiguration(HelloWorldMain.class));
+    R8Command command =
+        ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
+            .addMainDexRules(mainDexRules)
+            .build();
+    Path mainDexList = temp.newFile().toPath();
+    ToolHelper.runR8(command,
+        options -> {
+          options.printMainDexList = true;
+          options.printMainDexListFile = mainDexList;
+        });
+    // Main dex list with the single class.
+    assertEquals(
+        ImmutableList.of(HelloWorldMain.class.getTypeName().replace('.', '/') + ".class"),
+        FileUtils.readTextFile(mainDexList));
+  }
+}
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 44c2156..a0a77d1 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -3,15 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.maindexlist;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.D8Command;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Constants;
@@ -39,11 +39,13 @@
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
@@ -52,122 +54,101 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.Closer;
+import com.google.common.collect.Lists;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
 
-public class MainDexListTests {
-
-  private static final Path BASE =
-      Paths.get("src", "test", "java", "com", "android", "tools", "r8", "maindexlist");
-
-  private static boolean verifyApplications = true;
-  private static boolean regenerateApplications = false;
+public class MainDexListTests extends TestBase {
 
   private static final int MAX_METHOD_COUNT = Constants.U16BIT_MAX;
+
   private static final List<String> TWO_LARGE_CLASSES = ImmutableList.of("A", "B");
-  private static final String TWO_LARGE_CLASSES_APP = "two-large-classes.zip";
+  private static final int MANY_CLASSES_COUNT = 10000;
+  private static final int MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS = 2;
+  private static final int MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS = 10;
+  private static List<String> MANY_CLASSES;
 
-  private static final int MANY_CLASSES_COUNT = 1000;
-  private static final List<String> MANY_CLASSES;
-  private static final String MANY_CLASSES_APP = "many-classes.zip";
+  @ClassRule
+  public static TemporaryFolder generatedApplicationsFolder = new TemporaryFolder();
 
-  static {
+  // Generate the test applications in a @BeforeClass method, as they are used by several tests.
+  @BeforeClass
+  public static void generateTestApplications() throws Throwable {
     ImmutableList.Builder<String> builder = ImmutableList.builder();
     for (int i = 0; i < MANY_CLASSES_COUNT; ++i) {
       String pkg = i % 2 == 0 ? "a" : "b";
       builder.add(pkg + ".Class" + i);
     }
     MANY_CLASSES = builder.build();
+
+    // Generates an application with many classes, every even in one package and every odd in
+    // another. Keep the number of methods low enough for single dex application.
+    AndroidApp generated = generateApplication(
+        MANY_CLASSES, Constants.DEFAULT_ANDROID_API, MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS);
+    generated.write(getManyClassesSingleDexAppPath(), OutputMode.Indexed, false);
+
+    // Generates an application with many classes, every even in one package and every odd in
+    // another. Add enough methods so the application cannot fit into one dex file.
+    generated = generateApplication(
+        MANY_CLASSES, Constants.ANDROID_L_API, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
+    generated.write(getManyClassesMultiDexAppPath(), OutputMode.Indexed, false);
+
+    // Generates an application with two classes, each with the maximum possible number of methods.
+    generated = generateApplication(TWO_LARGE_CLASSES, Constants.ANDROID_N_API, MAX_METHOD_COUNT);
+    generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, false);
   }
 
-  public static Path getTwoLargeClassesAppPath() {
-    return BASE.resolve(TWO_LARGE_CLASSES_APP);
+  private static Path getTwoLargeClassesAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("two-large-classes.zip");
   }
 
-  public static Path getManyClassesAppPath() {
-    return BASE.resolve(MANY_CLASSES_APP);
+  private static Path getManyClassesSingleDexAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-mono.zip");
   }
 
-  // Generates an application with two classes, each with the maximum possible number of methods.
-  @Test
-  public void generateTwoLargeClasses() throws IOException, ExecutionException {
-    if (!verifyApplications && !regenerateApplications) {
-      return;
-    }
-    AndroidApp generated = generateApplication(TWO_LARGE_CLASSES, MAX_METHOD_COUNT);
-    if (regenerateApplications) {
-      generated.write(getTwoLargeClassesAppPath(), OutputMode.Indexed, true);
-    } else {
-      AndroidApp cached = AndroidApp.fromProgramFiles(getTwoLargeClassesAppPath());
-      compareToCachedVersion(cached, generated, TWO_LARGE_CLASSES_APP);
-    }
+  private static Path getManyClassesMultiDexAppPath() {
+    return generatedApplicationsFolder.getRoot().toPath().resolve("many-classes-stereo.zip");
   }
 
-  // Generates an application with many classes, every even in one package and every odd in another.
-  @Test
-  public void generateManyClasses() throws IOException, ExecutionException {
-    if (!verifyApplications && !regenerateApplications) {
-      return;
-    }
-    AndroidApp generated = generateApplication(MANY_CLASSES, 1);
-    if (regenerateApplications) {
-      generated.write(getManyClassesAppPath(), OutputMode.Indexed, true);
-    } else {
-      AndroidApp cached = AndroidApp.fromProgramFiles(getManyClassesAppPath());
-      compareToCachedVersion(cached, generated, MANY_CLASSES_APP);
-    }
-  }
-
-  private static void compareToCachedVersion(AndroidApp cached, AndroidApp generated, String name)
-      throws IOException {
-    assertEquals("On-disk cached app (" + name + ") differs in file count from regeneration"
-            + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
-        cached.getDexProgramResources().size(), generated.getDexProgramResources().size());
-    try (Closer closer = Closer.create()) {
-      for (int i = 0; i < cached.getDexProgramResources().size(); i++) {
-        byte[] cachedBytes =
-            ByteStreams.toByteArray(cached.getDexProgramResources().get(i).getStream(closer));
-        byte[] generatedBytes =
-            ByteStreams.toByteArray(generated.getDexProgramResources().get(i).getStream(closer));
-        assertArrayEquals("On-disk cached app differs in byte content from regeneration"
-                + "Set 'regenerateApplications = true' and rerun this test to update the cache.",
-            cachedBytes, generatedBytes);
-      }
-    }
-  }
-
-  @Rule
-  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
   @Test
+  public void checkGeneratedFileFitInSingleDexFile() {
+    assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_SINGLE_DEX_METHODS_PER_CLASS <= MAX_METHOD_COUNT);
+  }
+
+  @Test
+  public void checkGeneratedFileNeedsTwoDexFiles() {
+    assertTrue(MANY_CLASSES_COUNT * MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS > MAX_METHOD_COUNT);
+  }
+
+  @Test
   public void putFirstClassInMainDexList() throws Throwable {
-    verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES.subList(0, 1), getTwoLargeClassesAppPath(), false);
   }
 
   @Test
   public void putSecondClassInMainDexList() throws Throwable {
-    verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES.subList(1, 2), getTwoLargeClassesAppPath(), false);
   }
 
   @Test
   public void cannotFitBothIntoMainDex() throws Throwable {
     thrown.expect(CompilationError.class);
-    verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath());
+    verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
   }
 
   @Test
@@ -179,7 +160,28 @@
         mainDexBuilder.add(clazz);
       }
     }
-    verifyMainDexContains(mainDexBuilder.build(), getManyClassesAppPath());
+    verifyMainDexContains(mainDexBuilder.build(), getManyClassesSingleDexAppPath(), true);
+    verifyMainDexContains(mainDexBuilder.build(), getManyClassesMultiDexAppPath(), false);
+  }
+
+  @Test
+  public void singleClassInMainDex() throws Throwable {
+    ImmutableList<String> mainDex = ImmutableList.of(MANY_CLASSES.get(0));
+    verifyMainDexContains(mainDex, getManyClassesSingleDexAppPath(), true);
+    verifyMainDexContains(mainDex, getManyClassesMultiDexAppPath(), false);
+  }
+
+  @Test
+  public void allClassesInMainDex() throws Throwable {
+    // Degenerated case with an app thats fit into a single dex, and where the main dex list
+    // contains all classes.
+    verifyMainDexContains(MANY_CLASSES, getManyClassesSingleDexAppPath(), true);
+  }
+
+  @Test
+  public void cannotFitAllIntoMainDex() throws Throwable {
+    thrown.expect(CompilationError.class);
+    verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
   }
 
   @Test
@@ -207,11 +209,147 @@
     MainDexList.parse(mainDexList, factory);
   }
 
-  private static String typeToEntry(String type) {
-    return type.replace(".", "/") + CLASS_EXTENSION;
+  @Test
+  public void checkDeterminism() throws Exception {
+    // Synthesize a dex containing a few empty classes including some in the default package.
+    // Everything can fit easaly in a single dex file.
+    String[] classes = {
+        "A",
+        "B",
+        "C",
+        "D",
+        "E",
+        "F",
+        "A1",
+        "A2",
+        "A3",
+        "A4",
+        "A5",
+        "maindexlist/A",
+        "maindexlist/B",
+        "maindexlist/C",
+        "maindexlist/D",
+        "maindexlist/E",
+        "maindexlist/F",
+        "maindexlist/A1",
+        "maindexlist/A2",
+        "maindexlist/A3",
+        "maindexlist/A4",
+        "maindexlist/A5"
+    };
+    JasminBuilder jasminBuilder = new JasminBuilder();
+    for (String name : classes) {
+      jasminBuilder.addClass(name);
+    }
+    Path input = temp.newFolder().toPath().resolve("input.zip");
+    ToolHelper.runR8(jasminBuilder.build()).writeToZip(input, OutputMode.Indexed);
+
+    // Prepare different main dex lists.
+    ArrayList<Path> mainLists = new ArrayList<>();
+    // Lets first without a main dex list.
+    mainLists.add(null);
+
+    // List with all classes.
+    List<String> mainList = new ArrayList<>();
+    for (int i = 0; i < classes.length; i++) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Full list in reverse order
+    addMainListFile(mainLists, Lists.reverse(mainList));
+
+    // Partial list without first entries (those in default package).
+    mainList.clear();
+    for (int i = classes.length / 2; i < classes.length; i++) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Same in reverese order
+    addMainListFile(mainLists, Lists.reverse(mainList));
+
+    // Mixed partial list.
+    mainList.clear();
+    for (int i = 0; i < classes.length; i += 2) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Another different mixed partial list.
+    mainList.clear();
+    for (int i = 1; i < classes.length; i += 2) {
+      mainList.add(classes[i] + ".class");
+    }
+    addMainListFile(mainLists, mainList);
+
+    // Build with all main dex lists.
+    Path tmp = temp.getRoot().toPath();
+    for (int i = 0; i < mainLists.size(); i++) {
+      Path out = tmp.resolve(String.valueOf(i));
+      Files.createDirectories(out);
+      D8Command.Builder builder = D8Command.builder()
+          .addProgramFiles(input)
+          .setOutputPath(out);
+      if (mainLists.get(i) != null) {
+        builder.setMainDexListFile(mainLists.get(i));
+      }
+      ToolHelper.runD8(builder.build());
+    }
+
+    // Check: no secondary dex and resulting dex is always the same.
+    assertFalse(Files.exists(tmp.resolve(String.valueOf(0)).resolve("classes2.dex")));
+    byte[] ref = Files.readAllBytes(
+        tmp.resolve(String.valueOf(0)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+    for (int i = 1; i < mainLists.size(); i++) {
+      assertFalse(Files.exists(tmp.resolve(String.valueOf(i)).resolve("classes2.dex")));
+      byte[] checked = Files.readAllBytes(
+          tmp.resolve(String.valueOf(i)).resolve(FileUtils.DEFAULT_DEX_FILENAME));
+      assertArrayEquals(ref, checked);
+    }
   }
 
-  private void verifyMainDexContains(List<String> mainDex, Path app)
+  private void addMainListFile(ArrayList<Path> mainLists, List<String> content)
+      throws IOException {
+    Path listFile = temp.newFile().toPath();
+    FileUtils.writeTextFile(listFile, content);
+    mainLists.add(listFile);
+  }
+
+  private static String typeToEntry(String type) {
+    return type.replace(".", "/") + FileUtils.CLASS_EXTENSION;
+  }
+
+  private void failedToFindClassInExpectedFile(Path outDir, String clazz) throws IOException {
+    Files.list(outDir)
+        .filter(FileUtils::isDexFile)
+        .forEach(
+            p -> {
+              try {
+                DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
+                assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
+              } catch (IOException | ExecutionException e) {
+                e.printStackTrace();
+              }
+            });
+    fail("Failed to find class " + clazz + "in any file...");
+  }
+
+  private void assertMainDexClass(FoundClassSubject clazz, List<String> mainDex) {
+    if (!mainDex.contains(clazz.toString())) {
+      StringBuilder builder = new StringBuilder();
+      for (int i = 0; i < mainDex.size(); i++) {
+        builder.append(i == 0 ? "[" : ", ");
+        builder.append(mainDex.get(i));
+      }
+      builder.append("]");
+      fail("Class " + clazz + " found in main dex, " +
+          "only expected explicit main dex classes " + builder +" in main dex file");
+    }
+  }
+
+  private void doVerifyMainDexContains(
+      List<String> mainDex, Path app, boolean singleDexApp, boolean minimalMainDex)
       throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
     AndroidApp originalApp = AndroidApp.fromProgramFiles(app);
     DexInspector originalInspector = new DexInspector(originalApp);
@@ -219,53 +357,52 @@
       assertTrue("Class " + clazz + " does not exist in input",
           originalInspector.clazz(clazz).isPresent());
     }
-    Path outDir = temp.newFolder("out").toPath();
-    Path mainDexList = temp.getRoot().toPath().resolve("main-dex-list.txt");
+    Path outDir = temp.newFolder().toPath();
+    Path mainDexList = temp.newFile().toPath();
     FileUtils.writeTextFile(mainDexList, ListUtils.map(mainDex, MainDexListTests::typeToEntry));
-    Path packageDistribution = temp.getRoot().toPath().resolve("package.map");
-    FileUtils.writeTextFile(packageDistribution, ImmutableList.of("a.*:2", "b.*:1"));
     R8Command command =
         R8Command.builder()
             .addProgramFiles(app)
-            .setPackageDistributionFile(packageDistribution)
             .setMainDexListFile(mainDexList)
+            .setMinimalMainDex(minimalMainDex)
             .setOutputPath(outDir)
             .setTreeShaking(false)
             .setMinification(false)
             .build();
     ToolHelper.runR8(command);
-    assertTrue("Output run only produced one dex file. Invalid test",
-        1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+    if (!singleDexApp && !minimalMainDex) {
+      assertTrue("Output run only produced one dex file.",
+          1 < Files.list(outDir).filter(FileUtils::isDexFile).count());
+    }
     DexInspector inspector =
         new DexInspector(AndroidApp.fromProgramFiles(outDir.resolve("classes.dex")));
     for (String clazz : mainDex) {
       if (!inspector.clazz(clazz).isPresent()) {
-        Files.list(outDir)
-            .filter(FileUtils::isDexFile)
-            .forEach(
-                p -> {
-                  try {
-                    DexInspector i = new DexInspector(AndroidApp.fromProgramFiles(p));
-                    assertFalse("Found " + clazz + " in file " + p, i.clazz(clazz).isPresent());
-                  } catch (IOException | ExecutionException e) {
-                    e.printStackTrace();
-                  }
-                });
-        fail("Failed to find class " + clazz + "in any file...");
+        failedToFindClassInExpectedFile(outDir, clazz);
       }
     }
+    if (minimalMainDex) {
+      inspector.forAllClasses(clazz -> assertMainDexClass(clazz, mainDex));
+    }
   }
 
-  private static AndroidApp generateApplication(List<String> classes, int methodCount)
+  private void verifyMainDexContains(List<String> mainDex, Path app, boolean singleDexApp)
+      throws Throwable {
+    doVerifyMainDexContains(mainDex, app, singleDexApp, false);
+    doVerifyMainDexContains(mainDex, app, singleDexApp, true);
+  }
+
+  public static AndroidApp generateApplication(List<String> classes, int minApi, int methodCount)
       throws IOException, ExecutionException {
     Timing timing = new Timing("MainDexListTests");
     InternalOptions options = new InternalOptions();
+    options.minApiLevel = minApi;
     DexItemFactory factory = options.itemFactory;
     DexApplication.Builder builder = new DexApplication.Builder(factory, timing);
     for (String clazz : classes) {
       DexString desc = factory.createString(DescriptorUtils.javaTypeToDescriptor(clazz));
       DexType type = factory.createType(desc);
-      DexEncodedMethod[] virtualMethods = new DexEncodedMethod[methodCount];
+      DexEncodedMethod[] directMethods = new DexEncodedMethod[methodCount];
       for (int i = 0; i < methodCount; i++) {
         DexAccessFlags access = new DexAccessFlags();
         access.setPublic();
@@ -283,11 +420,11 @@
                 DexAnnotationSetRefList.empty(),
                 code);
         IRCode ir = code.buildIR(method, options);
-        RegisterAllocator allocator = new LinearScanRegisterAllocator(ir);
+        RegisterAllocator allocator = new LinearScanRegisterAllocator(ir, options);
         method.setCode(ir, allocator, factory);
-        virtualMethods[i] = method;
+        directMethods[i] = method;
       }
-      builder.addClassPromise(
+      builder.addProgramClass(
           new DexProgramClass(
               type,
               null,
@@ -298,8 +435,8 @@
               DexAnnotationSet.empty(),
               DexEncodedField.EMPTY_ARRAY,
               DexEncodedField.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              virtualMethods));
+              directMethods,
+              DexEncodedMethod.EMPTY_ARRAY));
     }
     DexApplication application = builder.build();
     AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
@@ -342,8 +479,8 @@
     }
 
     @Override
-    public boolean traceInstruction(int instructionIndex, IRBuilder builder) {
-      return true;
+    public int traceInstruction(int instructionIndex, IRBuilder builder) {
+      return instructionIndex;
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
index ae0f650..2549ff1 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -144,7 +144,7 @@
       String[] refList = new String(Files.readAllBytes(
           expectedMainDexList), StandardCharsets.UTF_8).split("\n");
       for (int i = 0; i < refList.length; i++) {
-        String reference = refList[i];
+        String reference = refList[i].trim();
         String computed = resultMainDexList.get(i);
         if (reference.contains("-$$Lambda$")) {
           // For lambda classes we check that there is a lambda class for the right containing
diff --git a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
deleted file mode 100644
index 3c1530d..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/many-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip b/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
deleted file mode 100644
index 97b5fde..0000000
--- a/src/test/java/com/android/tools/r8/maindexlist/two-large-classes.zip
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index 55bd373..c617c25 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -105,7 +105,8 @@
   }
 
   private static void inspectOriginalMain(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding.Test").method(DexInspector.MAIN);
+    MethodSubject main = inspector.clazz("memberrebinding.Memberrebinding")
+        .method(DexInspector.MAIN);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(MemberRebindingTest::coolInvokes);
     assertTrue(iterator.next().holder().is("memberrebinding.ClassAtBottomOfChain"));
@@ -132,7 +133,8 @@
   }
 
   private static void inspectMain(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding.Test").method(DexInspector.MAIN);
+    MethodSubject main = inspector.clazz("memberrebinding.Memberrebinding")
+        .method(DexInspector.MAIN);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(MemberRebindingTest::coolInvokes);
     assertTrue(iterator.next().holder().is("memberrebinding.ClassAtBottomOfChain"));
@@ -162,7 +164,8 @@
   }
 
   private static void inspectOriginalMain2(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding2.Test").method(DexInspector.MAIN);
+    MethodSubject main = inspector.clazz("memberrebinding2.Memberrebinding")
+        .method(DexInspector.MAIN);
     Iterator<FieldAccessInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isFieldAccess);
     // Run through instance put, static put, instance get and instance get.
@@ -177,7 +180,8 @@
   }
 
   private static void inspectMain2(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding2.Test").method(DexInspector.MAIN);
+    MethodSubject main = inspector.clazz("memberrebinding2.Memberrebinding")
+        .method(DexInspector.MAIN);
     Iterator<FieldAccessInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isFieldAccess);
     // Run through instance put, static put, instance get and instance get.
@@ -185,7 +189,7 @@
       assertTrue(iterator.next().holder().is("memberrebinding2.ClassAtBottomOfChain"));
       assertTrue(iterator.next().holder().is("memberrebinding2.ClassInMiddleOfChain"));
       assertTrue(iterator.next().holder().is("memberrebinding2.SuperClassOfAll"));
-      assertTrue(iterator.next().holder().is("memberrebinding2.subpackage.PackagePrivateClass"));
+      assertTrue(iterator.next().holder().is("memberrebinding2.subpackage.PublicClass"));
     }
     assertTrue(iterator.next().holder().is("java.lang.System"));
     assertFalse(iterator.hasNext());
@@ -195,7 +199,7 @@
       new MethodSignature("test", "void", new String[]{});
 
   private static void inspectOriginal3(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding3.Test").method(TEST);
+    MethodSubject main = inspector.clazz("memberrebinding3.Memberrebinding").method(TEST);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isInvoke);
     assertTrue(iterator.next().holder().is("memberrebinding3.ClassAtBottomOfChain"));
@@ -205,7 +209,7 @@
   }
 
   private static void inspect3(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding3.Test").method(TEST);
+    MethodSubject main = inspector.clazz("memberrebinding3.Memberrebinding").method(TEST);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isInvoke);
     assertTrue(iterator.next().holder().is("memberrebinding3.ClassAtBottomOfChain"));
@@ -215,19 +219,19 @@
   }
 
   private static void inspectOriginal4(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding4.Test").method(TEST);
+    MethodSubject main = inspector.clazz("memberrebinding4.Memberrebinding").method(TEST);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isInvoke);
-    assertTrue(iterator.next().holder().is("memberrebinding4.Test$Inner"));
+    assertTrue(iterator.next().holder().is("memberrebinding4.Memberrebinding$Inner"));
     assertTrue(iterator.next().holder().is("memberrebinding4.subpackage.PublicInterface"));
     assertFalse(iterator.hasNext());
   }
 
   private static void inspect4(DexInspector inspector) {
-    MethodSubject main = inspector.clazz("memberrebinding4.Test").method(TEST);
+    MethodSubject main = inspector.clazz("memberrebinding4.Memberrebinding").method(TEST);
     Iterator<InvokeInstructionSubject> iterator =
         main.iterateInstructions(InstructionSubject::isInvoke);
-    assertTrue(iterator.next().holder().is("memberrebinding4.Test$Inner"));
+    assertTrue(iterator.next().holder().is("memberrebinding4.Memberrebinding$Inner"));
     assertTrue(iterator.next().holder().is("memberrebinding4.subpackage.PublicInterface"));
     assertFalse(iterator.hasNext());
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 5d5bc17..05b06af 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -11,6 +11,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexAccessFlags;
 import com.android.tools.r8.graph.DexItemFactory;
 import java.io.IOException;
@@ -45,6 +46,8 @@
       INVALID_PROGUARD_DIR + "including-2.flags";
   private static final String LIBRARY_JARS =
       VALID_PROGUARD_DIR + "library-jars.flags";
+  private static final String LIBRARY_JARS_WIN =
+          VALID_PROGUARD_DIR + "library-jars-win.flags";
   private static final String SEEDS =
       VALID_PROGUARD_DIR + "seeds.flags";
   private static final String SEEDS_2 =
@@ -59,6 +62,8 @@
       VALID_PROGUARD_DIR + "dontskipnonpubliclibraryclasses.flags";
   private static final String DONT_SKIP_NON_PUBLIC_LIBRARY_CLASS_MEMBERS =
       VALID_PROGUARD_DIR + "dontskipnonpubliclibraryclassmembers.flags";
+  private static final String OVERLOAD_AGGRESIVELY =
+      VALID_PROGUARD_DIR + "overloadaggressively.flags";
   private static final String DONT_OPTIMIZE =
       VALID_PROGUARD_DIR + "dontoptimize.flags";
   private static final String SKIP_NON_PUBLIC_LIBRARY_CLASSES =
@@ -281,7 +286,11 @@
   @Test
   public void parseLibraryJars() throws IOException, ProguardRuleParserException {
     ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
-    parser.parse(Paths.get(LIBRARY_JARS));
+    if (!ToolHelper.isLinux() && !ToolHelper.isMac()) {
+      parser.parse(Paths.get(LIBRARY_JARS_WIN));
+    } else {
+      parser.parse(Paths.get(LIBRARY_JARS));
+    }
     assertEquals(4, parser.getConfig().getLibraryjars().size());
   }
 
@@ -332,6 +341,13 @@
   }
 
   @Test
+  public void parseOverloadAggressively()
+      throws IOException, ProguardRuleParserException {
+    ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
+    parser.parse(Paths.get(OVERLOAD_AGGRESIVELY));
+  }
+
+  @Test
   public void parseDontOptimize()
       throws IOException, ProguardRuleParserException {
     ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
@@ -372,4 +388,15 @@
     );
     parser.parse(proguardConfig);
   }
+
+  @Test
+  public void parseCustomFlags() throws Exception {
+    ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
+    // Custom Proguard flags -runtype and -laststageoutput are ignored.
+    Path proguardConfig = writeTextToTempFile(
+        "-runtype FINAL                    ",
+        "-laststageoutput /some/file/name  "
+    );
+    parser.parse(proguardConfig);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index f5b2b1a..8f46572 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -11,13 +11,16 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.errors.CompilationError;
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -95,7 +98,11 @@
     actualMapping = new String(Files.readAllBytes(outputmapping), StandardCharsets.UTF_8);
     String refMapping = new String(Files.readAllBytes(
         Paths.get(EXAMPLES_DIR, "shaking1", "print-mapping.ref")), StandardCharsets.UTF_8);
-    Assert.assertEquals(refMapping, actualMapping);
+    Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
   }
 
+  private static String sorted(String str) {
+    return new BufferedReader(new StringReader(str))
+        .lines().sorted().collect(Collectors.joining("\n"));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index 4530e2e..0904569 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexAccessFlags;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -126,7 +127,11 @@
             .addLibraryFiles(ListUtils.map(libs, Paths::get))
             .setMinification(minify)
             .build();
-    ToolHelper.runR8(command, options -> options.inlineAccessors = inline);
+    ToolHelper.runR8(command, options -> {
+      options.inlineAccessors = inline;
+      options.printMapping = true;
+      options.printMappingFile = out.resolve(AndroidApp.DEFAULT_PROGUARD_MAP_FILE);
+    });
   }
 
   public static void shaking1HasNoClassUnused(DexInspector inspector) {
@@ -542,7 +547,9 @@
             "assumevalues3",
             "assumevalues4",
             "assumevalues5",
-            "annotationremoval");
+            "annotationremoval",
+            "memberrebinding2",
+            "memberrebinding3");
 
     // Keys can be the name of the test or the name of the test followed by a colon and the name
     // of the keep file.
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index 5e67ad1..ca30f76 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -93,7 +93,7 @@
             assertTrue(
                 defBlock.getInstructions().get(defBlock.getInstructions().size() - 2).isInvoke());
             for (BasicBlock returnPredecessor : block.getPredecessors()) {
-              if (defBlock.isCatchSuccessor(returnPredecessor)) {
+              if (defBlock.hasCatchSuccessor(returnPredecessor)) {
                 hasExceptionalPredecessor = true;
               } else if (defBlock == returnPredecessor) {
                 // Normal flow goes to return.
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
index b1fd38f..66d0304 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliDisassembleTest.java
@@ -68,7 +68,9 @@
             "    mul-int/2addr       v2, v1\n" +
             "    div-int/2addr       v3, v2\n" +
             "    return              v3\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -122,7 +124,9 @@
             "      0x00000001 -> :label_5  # 1\n" +
             "      0x00000002 -> :label_7  # 2\n" +
             "    .end sparse-switch\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -176,7 +180,9 @@
             "      :label_5\n" +
             "      :label_7\n" +
             "    .end packed-switch\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -217,7 +223,9 @@
             "      0x02  # 2\n" +
             "      0xff  # 255\n" +
             "    .end array-data\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -258,7 +266,9 @@
             "      0x0002  # 2\n" +
             "      0xffff  # 65535\n" +
             "    .end array-data\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -299,7 +309,9 @@
             "      0x00000002  # 2\n" +
             "      0xffffffff  # 4294967295\n" +
             "    .end array-data\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
@@ -340,14 +352,15 @@
             "      0x0000000000000002  # 2\n" +
             "      0xffffffffffffffff  # -1\n" +
             "    .end array-data\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
     roundTripRawSmali(expected);
   }
 
-
   @Test
   public void interfaceClass() {
     SmaliBuilder builder = new SmaliBuilder();
@@ -362,7 +375,33 @@
             ".super Ljava/lang/Object;\n" +
             "\n" +
             ".method public abstract test()I\n" +
-            ".end method\n";
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
+
+    assertEquals(expected, application.smali(new InternalOptions()));
+
+    roundTripRawSmali(expected);
+  }
+
+  @Test
+  public void implementsInterface() {
+    SmaliBuilder builder = new SmaliBuilder();
+    builder.addClass("Test", "java.lang.Object", ImmutableList.of("java.util.List"));
+    builder.addAbstractMethod("int", "test", ImmutableList.of());
+    DexApplication application = buildApplication(builder);
+    assertEquals(1, Iterables.size(application.classes()));
+
+    String expected =
+        ".class public LTest;\n" +
+            "\n" +
+            ".super Ljava/lang/Object;\n" +
+            ".implements Ljava/util/List;\n" +
+            "\n" +
+            ".method public abstract test()I\n" +
+            ".end method\n" +
+            "\n" +
+            "# End of class LTest;\n";
 
     assertEquals(expected, application.smali(new InternalOptions()));
 
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index 3665a86..6b377f4 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.Smali;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -77,11 +78,27 @@
     abstract class Builder {
       String name;
       String superName;
+      List<String> implementedInterfaces;
       List<String> source = new ArrayList<>();
 
-      Builder(String name, String superName) {
+      Builder(String name, String superName, List<String> implementedInterfaces) {
         this.name = name;
         this.superName = superName;
+        this.implementedInterfaces = implementedInterfaces;
+      }
+
+      protected void appendSuper(StringBuilder builder) {
+        builder.append(".super ");
+        builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
+        builder.append("\n");
+      }
+
+      protected void appendImplementedInterfaces(StringBuilder builder) {
+        for (String implementedInterface : implementedInterfaces) {
+          builder.append(".implements ");
+          builder.append(DescriptorUtils.javaTypeToDescriptor(implementedInterface));
+          builder.append("\n");
+        }
       }
 
       protected void writeSource(StringBuilder builder) {
@@ -94,8 +111,8 @@
 
     public class ClassBuilder extends Builder {
 
-      ClassBuilder(String name, String superName) {
-        super(name, superName);
+      ClassBuilder(String name, String superName, List<String> implementedInterfaces) {
+        super(name, superName, implementedInterfaces);
       }
 
       public String toString() {
@@ -103,9 +120,9 @@
         builder.append(".class public ");
         builder.append(DescriptorUtils.javaTypeToDescriptor(name));
         builder.append("\n");
-        builder.append(".super ");
-        builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
-        builder.append("\n\n");
+        appendSuper(builder);
+        appendImplementedInterfaces(builder);
+        builder.append("\n");
         writeSource(builder);
         return builder.toString();
       }
@@ -114,7 +131,7 @@
     public class InterfaceBuilder extends Builder {
 
       InterfaceBuilder(String name, String superName) {
-        super(name, superName);
+        super(name, superName, ImmutableList.of());
       }
 
       public String toString() {
@@ -122,9 +139,9 @@
         builder.append(".class public interface abstract ");
         builder.append(DescriptorUtils.javaTypeToDescriptor(name));
         builder.append("\n");
-        builder.append(".super ");
-        builder.append(DescriptorUtils.javaTypeToDescriptor(superName));
-        builder.append("\n\n");
+        appendSuper(builder);
+        appendImplementedInterfaces(builder);
+        builder.append("\n");
         writeSource(builder);
         return builder.toString();
       }
@@ -162,9 +179,13 @@
     }
 
     public void addClass(String name, String superName) {
+      addClass(name, superName, ImmutableList.of());
+    }
+
+    public void addClass(String name, String superName, List<String> implementedInterfaces) {
       assert !classes.containsKey(name);
       currentClassName = name;
-      classes.put(name, new ClassBuilder(name, superName));
+      classes.put(name, new ClassBuilder(name, superName, implementedInterfaces));
     }
 
     public void addInterface(String name) {
diff --git a/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java b/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
index 0344a27..fa3497b 100644
--- a/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
+++ b/src/test/java/com/android/tools/r8/utils/ArtCommandBuilderTest.java
@@ -9,10 +9,17 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
 import org.junit.Test;
 
 public class ArtCommandBuilderTest {
 
+  @Before
+  public void setUp() {
+    Assume.assumeTrue(ToolHelper.artSupported());
+  }
+
   @Test
   public void noArguments() {
     ArtCommandBuilder builder = new ArtCommandBuilder();
diff --git a/src/test/proguard/valid/library-jars-win.flags b/src/test/proguard/valid/library-jars-win.flags
new file mode 100644
index 0000000..c6ffbb4
--- /dev/null
+++ b/src/test/proguard/valid/library-jars-win.flags
@@ -0,0 +1,2 @@
+-libraryjars some/library.jar
+-libraryjars some/library.jar;some/other/library.jar;yet/another/library.jar
\ No newline at end of file
diff --git a/src/test/proguard/valid/overloadaggressively.flags b/src/test/proguard/valid/overloadaggressively.flags
new file mode 100644
index 0000000..312ebff
--- /dev/null
+++ b/src/test/proguard/valid/overloadaggressively.flags
@@ -0,0 +1,5 @@
+# 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.
+
+-overloadaggressively
diff --git a/third_party/android_cts_baseline.tar.gz.sha1 b/third_party/android_cts_baseline.tar.gz.sha1
index 7126671..5efd8e5 100644
--- a/third_party/android_cts_baseline.tar.gz.sha1
+++ b/third_party/android_cts_baseline.tar.gz.sha1
@@ -1 +1 @@
-3914f251fc8b8669b2ec92766696267c1a9ac7ff
\ No newline at end of file
+499d86b9b9eba837261c2d10e5b76f3a3a9a9952
\ No newline at end of file
diff --git a/third_party/android_jar/lib-v21.tar.gz.sha1 b/third_party/android_jar/lib-v21.tar.gz.sha1
new file mode 100644
index 0000000..8d42abc
--- /dev/null
+++ b/third_party/android_jar/lib-v21.tar.gz.sha1
@@ -0,0 +1 @@
+83d20da35ba7b7b1a8b48a2da8dfe3b95d59392a
\ No newline at end of file
diff --git a/third_party/gmail/gmail_android_170604.16.tar.gz.sha1 b/third_party/gmail/gmail_android_170604.16.tar.gz.sha1
new file mode 100644
index 0000000..f57ba90
--- /dev/null
+++ b/third_party/gmail/gmail_android_170604.16.tar.gz.sha1
@@ -0,0 +1 @@
+161c569821a5c9b4cb8e99de764f3449191af084
\ No newline at end of file
diff --git a/third_party/gmscore/latest.tar.gz.sha1 b/third_party/gmscore/latest.tar.gz.sha1
new file mode 100644
index 0000000..0a0d4d4
--- /dev/null
+++ b/third_party/gmscore/latest.tar.gz.sha1
@@ -0,0 +1 @@
+d38c56b1c4583393fda2bff933a8d09443e5c792
\ No newline at end of file
diff --git a/third_party/jctf.tar.gz.sha1 b/third_party/jctf.tar.gz.sha1
index 73cc756..35ec1ba 100644
--- a/third_party/jctf.tar.gz.sha1
+++ b/third_party/jctf.tar.gz.sha1
@@ -1 +1 @@
-d9256a5987d50372f916d367cb8e8e5286a10caa
+93c13a164ca54db72273642d27308e325526fd38
\ No newline at end of file
diff --git a/third_party/proguard/proguard_internal_159423826.tar.gz.sha1 b/third_party/proguard/proguard_internal_159423826.tar.gz.sha1
new file mode 100644
index 0000000..8fdbf19
--- /dev/null
+++ b/third_party/proguard/proguard_internal_159423826.tar.gz.sha1
@@ -0,0 +1 @@
+933e970c611a306ef5d755234820188bc50c4c60
\ No newline at end of file
diff --git a/third_party/youtube/youtube.android_11.47.tar.gz.sha1 b/third_party/youtube/youtube.android_11.47.tar.gz.sha1
deleted file mode 100644
index db94984..0000000
--- a/third_party/youtube/youtube.android_11.47.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-3c763fc5b033497dabdfff2ecd27475cfa7759a4
\ No newline at end of file
diff --git a/third_party/youtube/youtube.android_12.22.tar.gz.sha1 b/third_party/youtube/youtube.android_12.22.tar.gz.sha1
new file mode 100644
index 0000000..8f6813c
--- /dev/null
+++ b/third_party/youtube/youtube.android_12.22.tar.gz.sha1
@@ -0,0 +1 @@
+73c4880898d734064815d0426d8fe84ee6d075b4
\ No newline at end of file
diff --git a/tools/compare_cts_results.py b/tools/compare_cts_results.py
index 95e29d1..00901ec 100755
--- a/tools/compare_cts_results.py
+++ b/tools/compare_cts_results.py
@@ -9,9 +9,10 @@
 from os.path import basename
 import argparse
 import os
-import re
 import sys
 
+import utils
+
 class Module:
   def __init__(self):
     self.test_cases = {}
@@ -90,35 +91,19 @@
 
 # Read CTS test_result.xml from file and merge into result_tree
 def add_to_result_tree(result_tree, file_xml, file_idx):
-  re_module = re.compile('<Module name="([^"]*)"')
-  re_test_case = re.compile('<TestCase name="([^"]*)"')
-  re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
   module = None
   test_case = None
-  with open(file_xml) as f:
-    for line in f:
-      m = re_module.search(line)
-      if m:
-        module_name = m.groups()[0]
-        module = result_tree.setdefault(module_name, Module())
-        module.set_file_index_present(file_idx)
-        continue
-
-      m = re_test_case.search(line)
-      if m:
-        test_case_name = m.groups()[0]
-        test_case = module.get_test_case_maybe_create(test_case_name)
-        test_case.set_file_index_present(file_idx)
-        continue
-
-      m = re_test.search(line)
-      if m:
-        outcome = m.groups()[0]
-        test_name = m.groups()[1]
-        assert outcome in ["fail", "pass"]
-
-        v = test_case.get_test_maybe_create(test_name)
-        v.set_file_index_outcome(outcome == 'pass', file_idx)
+  for x in utils.read_cts_test_result(file_xml):
+    if type(x) is utils.CtsModule:
+      module = result_tree.setdefault(x.name, Module())
+      module.set_file_index_present(file_idx)
+    elif type(x) is utils.CtsTestCase:
+      test_case = module.get_test_case_maybe_create(x.name)
+      test_case.set_file_index_present(file_idx)
+    else:
+      assert(type(x) is utils.CtsTest)
+      v = test_case.get_test_maybe_create(x.name)
+      v.set_file_index_outcome(x.outcome, file_idx)
 
 # main tree_report function
 def tree_report(result_tree, files, diff_only):
diff --git a/tools/gmail_data.py b/tools/gmail_data.py
new file mode 100644
index 0000000..d09a3e8
--- /dev/null
+++ b/tools/gmail_data.py
@@ -0,0 +1,35 @@
+# 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.
+
+import glob
+import os
+import utils
+
+THIRD_PARTY = os.path.join(utils.REPO_ROOT, 'third_party')
+BASE = os.path.join(THIRD_PARTY, 'gmail')
+
+V170604_16_BASE = os.path.join(BASE, 'gmail_android_170604.16')
+V170604_16_PREFIX = os.path.join(V170604_16_BASE, 'Gmail_release_unstripped')
+
+# NOTE: We always use android.jar for SDK v25 for now.
+ANDROID_JAR = os.path.join(THIRD_PARTY, 'android_jar', 'lib-v25', 'android.jar')
+
+VERSIONS = {
+  '170604.16': {
+    'dex' : {
+      'inputs': [os.path.join(V170604_16_BASE, 'Gmail_release_unsigned.apk')],
+      'pgmap': '%s_proguard.map' % V170604_16_PREFIX,
+      'libraries' : [ANDROID_JAR],
+      'r8-flags': '--ignore-missing-classes',
+    },
+    'deploy' : {
+      'inputs': ['%s_deploy.jar' % V170604_16_PREFIX],
+      'pgconf': ['%s_proguard.config' % V170604_16_PREFIX],
+    },
+    'proguarded' : {
+      'inputs': ['%s_proguard.jar' % V170604_16_PREFIX],
+      'pgmap': '%s_proguard.map' % V170604_16_PREFIX,
+    }
+  },
+}
diff --git a/tools/gradle.py b/tools/gradle.py
index 1945557..f30b3f4 100755
--- a/tools/gradle.py
+++ b/tools/gradle.py
@@ -14,7 +14,10 @@
 
 GRADLE_DIR = os.path.join(utils.REPO_ROOT, 'third_party', 'gradle')
 GRADLE_SHA1 = os.path.join(GRADLE_DIR, 'gradle.tar.gz.sha1')
-GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle')
+if utils.IsWindows():
+  GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle.bat')
+else:
+  GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle')
 
 def PrintCmd(s):
   if type(s) is list:
diff --git a/tools/proguard.py b/tools/proguard.py
new file mode 100755
index 0000000..58c049f
--- /dev/null
+++ b/tools/proguard.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# 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.
+
+# Run ProGuard, Google's internal version
+
+from __future__ import print_function
+import os
+import subprocess
+import sys
+
+import utils
+
+PROGUARD_JAR = os.path.join(utils.REPO_ROOT, 'third_party', 'proguard',
+    'proguard_internal_159423826', 'ProGuard_deploy.jar')
+
+def run(args):
+  cmd = ['java', '-jar', PROGUARD_JAR]
+  cmd.extend(args)
+  print('-- ' + ' '.join(cmd))
+  subprocess.check_call(cmd)
+
+def Main():
+  run(sys.argv[1:])
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/run-r8-on-gmscore.py b/tools/run-r8-on-gmscore.py
index c00a58d..6d09760 100755
--- a/tools/run-r8-on-gmscore.py
+++ b/tools/run-r8-on-gmscore.py
@@ -5,7 +5,8 @@
 
 import sys
 
-import run_r8_on_app
+import run_on_app
 
 if __name__ == '__main__':
-  sys.exit(run_r8_on_app.main())
+  # Default compiler is R8.
+  sys.exit(run_on_app.main())
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
new file mode 100755
index 0000000..ddc1e47
--- /dev/null
+++ b/tools/run_on_app.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# 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.
+
+import optparse
+import os
+import r8
+import d8
+import sys
+import utils
+import time
+
+import gmscore_data
+import youtube_data
+import gmail_data
+
+TYPES = ['dex', 'deploy', 'proguarded']
+APPS = ['gmscore', 'youtube', 'gmail']
+
+def ParseOptions():
+  result = optparse.OptionParser()
+  result.add_option('--compiler',
+                    help='',
+                    default='r8',
+                    choices=['d8', 'r8'])
+  result.add_option('--app',
+                    help='',
+                    default='gmscore',
+                    choices=APPS)
+  result.add_option('--type',
+                    help='Default for R8: deploy, for D8: proguarded',
+                    choices=TYPES)
+  result.add_option('--out',
+                    help='',
+                    default=os.getcwd())
+  result.add_option('--no-build',
+                    help='',
+                    default=False,
+                    action='store_true')
+  result.add_option('--no-libraries',
+                    help='',
+                    default=False,
+                    action='store_true')
+  result.add_option('--no-debug',
+                    help='Run without debug asserts.',
+                    default=False,
+                    action='store_true')
+  result.add_option('--version',
+                    help='')
+  result.add_option('-k',
+                    help='Override the default ProGuard keep rules')
+  result.add_option('--compiler-flags',
+                    help='Additional option(s) for the compiler. ' +
+                         'If passing several options use a quoted string.')
+  result.add_option('--r8-flags',
+                    help='Additional option(s) for the compiler. ' +
+                         'Same as --compiler-flags, keeping it for backward compatibility. ' +
+                         'If passing several options use a quoted string.')
+  result.add_option('--track-memory-to-file',
+                    help='Track how much memory the jvm is using while ' +
+                    ' compiling. Output to the specified file.')
+  result.add_option('--profile',
+                    help='Profile R8 run.',
+                    default=False,
+                    action='store_true')
+  result.add_option('--dump-args-file',
+                    help='Dump a file with the arguments for the specified ' +
+                    'configuration. For use as a @<file> argument to perform ' +
+                    'the run.')
+  result.add_option('--print-runtimeraw',
+                    metavar='BENCHMARKNAME',
+                    help='Prints the line \'<BENCHMARKNAME>(RunTimeRaw):' +
+                         ' <elapsed> ms\' at the end where <elapsed> is' +
+                         ' the elapsed time in milliseconds.')
+  return result.parse_args()
+
+# Most apps have the -printmapping and -printseeds in the Proguard
+# configuration. However we don't want to write these files in these
+# locations. Instead generate an auxiliary Proguard configuration
+# placing these two output files together with the dex output.
+def GenerateAdditionalProguardConfiguration(temp, outdir):
+  name = "output.config"
+  with open(os.path.join(temp, name), 'w') as file:
+    file.write('-printmapping ' + os.path.join(outdir, 'proguard.map') + "\n")
+    file.write('-printseeds ' + os.path.join(outdir, 'proguard.seeds') + "\n")
+    return os.path.abspath(file.name)
+
+def main():
+  app_provided_pg_conf = False;
+  (options, args) = ParseOptions()
+  outdir = options.out
+  data = None
+  if options.app == 'gmscore':
+    options.version = options.version or 'v9'
+    data = gmscore_data
+  elif options.app == 'youtube':
+    options.version = options.version or '12.22'
+    data = youtube_data
+  elif options.app == 'gmail':
+    options.version = options.version or '170604.16'
+    data = gmail_data
+  else:
+    raise 'Unexpected'
+
+  if not options.version in data.VERSIONS.keys():
+    print 'No version %s for application %s' % (options.version, options.app)
+    print 'Valid versions are %s' % data.VERSIONS.keys()
+    return 1
+
+  version = data.VERSIONS[options.version]
+
+  if not options.type:
+    options.type = 'deploy' if options.compiler == 'r8' \
+        else 'proguarded'
+
+  if options.type not in version:
+    print 'No type %s for version %s' % (options.type, options.version)
+    print 'Valid types are %s' % version.keys()
+    return 1
+  values = version[options.type]
+  inputs = None
+  # For R8 'deploy' the JAR is located using the Proguard configuration -injars option.
+  if 'inputs' in values and (options.compiler != 'r8' or options.type != 'deploy'):
+    inputs = values['inputs']
+
+  args.extend(['--output', outdir])
+
+  if options.compiler == 'r8':
+    if 'pgmap' in values:
+      args.extend(['--pg-map', values['pgmap']])
+    if 'pgconf' in values and not options.k:
+      for pgconf in values['pgconf']:
+        args.extend(['--pg-conf', pgconf])
+        app_provided_pg_conf = True
+    if options.k:
+      args.extend(['--pg-conf', options.k])
+    if 'multidexrules' in values:
+      for rules in values['multidexrules']:
+        args.extend(['--multidex-rules', rules])
+
+  if not options.no_libraries and 'libraries' in values:
+    for lib in values['libraries']:
+      args.extend(['--lib', lib])
+
+  if not outdir.endswith('.zip') and not outdir.endswith('.jar') and not os.path.exists(outdir):
+    os.makedirs(outdir)
+
+  if options.compiler == 'r8':
+    if 'r8-flags' in values:
+      args.extend(values['r8-flags'].split(' '))
+
+  if options.compiler_flags:
+    args.extend(options.compiler_flags.split(' '))
+  if options.r8_flags:
+    args.extend(options.r8_flags.split(' '))
+
+  if inputs:
+    args.extend(inputs)
+
+  t0 = time.time()
+
+  if options.dump_args_file:
+    with open(options.dump_args_file, 'w') as args_file:
+      args_file.writelines([arg + os.linesep for arg in args])
+  else:
+    if options.compiler == 'd8':
+      d8.run(args, not options.no_build, not options.no_debug, options.profile,
+             options.track_memory_to_file)
+    else:
+      with utils.TempDir() as temp:
+        if app_provided_pg_conf:
+          # Ensure that output of -printmapping and -printseeds go to the output
+          # location and not where the app Proguard configuration places them.
+          if outdir.endswith('.zip') or outdir.endswith('.jar'):
+            pg_outdir = os.path.dirname(outdir)
+          else:
+            pg_outdir = outdir
+          additional_pg_conf = GenerateAdditionalProguardConfiguration(
+              temp, os.path.abspath(pg_outdir))
+          args.extend(['--pg-conf', additional_pg_conf])
+        r8.run(args, not options.no_build, not options.no_debug, options.profile,
+               options.track_memory_to_file)
+
+  if options.print_runtimeraw:
+    print('{}(RunTimeRaw): {} ms'
+        .format(options.print_runtimeraw, 1000.0 * (time.time() - t0)))
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/run_proguard_dx_on_gmscore.py b/tools/run_proguard_dx_on_gmscore.py
new file mode 100755
index 0000000..a67868b
--- /dev/null
+++ b/tools/run_proguard_dx_on_gmscore.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# 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.
+
+# Run ProGuard and the DX tool on GmsCore V10.
+
+from __future__ import print_function
+from os import makedirs
+from os.path import exists, join
+from subprocess import check_call
+import argparse
+import gmscore_data
+import os
+import stat
+import sys
+import time
+
+import proguard
+import utils
+
+BLAZE_BUILD_DIR = join(gmscore_data.V10_BASE,
+    'blaze-out/intel-linux-android-4.8-libcxx-x86-opt/bin/java/com/google/'
+    'android/gmscore/integ')
+PROGUARDED_OUTPUT = join(BLAZE_BUILD_DIR,
+    'GmsCore_prod_alldpi_release_all_locales_proguard.jar')
+GMSCORE_SEEDS_FILE = join(BLAZE_BUILD_DIR,
+    'GmsCore_prod_alldpi_release_all_locales_proguard.seeds')
+DX_EXECUTABLE = join(utils.REPO_ROOT, 'tools/linux/dx/bin/dx')
+
+def parse_arguments():
+  parser = argparse.ArgumentParser(
+      description = 'Run ProGuard and the DX tool on GmsCore V10.')
+  parser.add_argument('--out',
+      help = 'Output directory for the DX tool.',
+      default = os.getcwd())
+  parser.add_argument('--print-runtimeraw',
+      metavar = 'BENCHMARKNAME',
+      help = 'Prints the line \'<BENCHMARKNAME>(RunTimeRaw): <elapsed>' +
+             ' ms\' at the end where <elapsed> is the elapsed time in' +
+             ' milliseconds.')
+  return parser.parse_args()
+
+def Main():
+  options = parse_arguments()
+
+  outdir = options.out
+
+  args = ['-forceprocessing']
+
+  if not outdir.endswith('.zip') and not outdir.endswith('.jar') \
+      and not exists(outdir):
+    makedirs(outdir)
+
+  version = gmscore_data.VERSIONS['v10']
+  values = version['deploy']
+  assert 'pgconf' in values
+
+  for pgconf in values['pgconf']:
+    args.extend(['@' + pgconf])
+
+  # Remove write-protection from seeds file. The seeds file is an output of
+  # ProGuard so it aborts if this is not writeable.
+  st = os.stat(GMSCORE_SEEDS_FILE)
+  os.chmod(GMSCORE_SEEDS_FILE,
+      st.st_mode | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
+
+  t0 = time.time()
+
+  proguard.run(args)
+
+  # run dex on the result
+  check_call([DX_EXECUTABLE, '-JXmx5256M', '--multi-dex', '--output=' + outdir,
+      '--dex', PROGUARDED_OUTPUT])
+
+  if options.print_runtimeraw:
+    print('{}(RunTimeRaw): {} ms'
+        .format(options.print_runtimeraw, 1000.0 * (time.time() - t0)))
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/run_r8_on_app.py b/tools/run_r8_on_app.py
deleted file mode 100755
index 736d428..0000000
--- a/tools/run_r8_on_app.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python
-# 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.
-
-import optparse
-import os
-import r8
-import sys
-
-import gmscore_data
-import youtube_data
-
-TYPES = ['dex', 'deploy', 'proguarded']
-APPS = ['gmscore', 'youtube']
-
-def ParseOptions():
-  result = optparse.OptionParser()
-  result.add_option('--app',
-                    help='',
-                    default='gmscore',
-                    choices=APPS)
-  result.add_option('--type',
-                    help='',
-                    default='deploy',
-                    choices=TYPES)
-  result.add_option('--out',
-                    help='',
-                    default=os.getcwd())
-  result.add_option('--no-build',
-                    help='',
-                    default=False,
-                    action='store_true')
-  result.add_option('--no-libraries',
-                    help='',
-                    default=False,
-                    action='store_true')
-  result.add_option('--no-debug',
-                    help='Run without debug asserts.',
-                    default=False,
-                    action='store_true')
-  result.add_option('--version',
-                    help='')
-  result.add_option('-k',
-                    help='Override the default ProGuard keep rules')
-  result.add_option('--r8-flags',
-                    help='Additional option(s) for R8. ' +
-                         'If passing several options use a quoted string.')
-  result.add_option('--track-memory-to-file',
-                    help='Track how much memory the jvm is using while ' +
-                    ' compiling. Output to the specified file.')
-  result.add_option('--profile',
-                    help='Profile R8 run.',
-                    default=False,
-                    action='store_true')
-  result.add_option('--dump-args-file',
-                    help='Dump a file with the arguments for the specified ' +
-                    'configuration. For use as a @<file> argument to perform ' +
-                    'the run.')
-  return result.parse_args()
-
-def main():
-  (options, args) = ParseOptions()
-  outdir = options.out
-  data = None
-  if options.app == 'gmscore':
-    options.version = options.version or 'v9'
-    data = gmscore_data
-  elif options.app == 'youtube':
-    options.version = options.version or '12.10'
-    data = youtube_data
-  else:
-    raise 'Unexpected'
-
-  if not options.version in data.VERSIONS.keys():
-    print 'No version %s for application %s' % (options.version, options.app)
-    print 'Valid versions are %s' % data.VERSIONS.keys()
-    return 1
-
-  version = data.VERSIONS[options.version]
-
-  if options.type not in version:
-    print 'No type %s for version %s' % (options.type, options.version)
-    print 'Valid types are %s' % version.keys()
-    return 1
-  values = version[options.type]
-  inputs = None
-  # For 'deploy' the JAR is located using the Proguard configuration -injars option.
-  if 'inputs' in values and options.type != 'deploy':
-    inputs = values['inputs']
-
-  args.extend(['--output', outdir])
-  if 'pgmap' in values:
-    args.extend(['--pg-map', values['pgmap']])
-  if 'pgconf' in values and not options.k:
-    for pgconf in values['pgconf']:
-      args.extend(['--pg-conf', pgconf])
-  if options.k:
-    args.extend(['--pg-conf', options.k])
-  if not options.no_libraries and 'libraries' in values:
-    for lib in values['libraries']:
-      args.extend(['--lib', lib])
-
-  if not outdir.endswith('.zip') and not outdir.endswith('.jar') and not os.path.exists(outdir):
-    os.makedirs(outdir)
-
-  if 'r8-flags' in values:
-    args.extend(values['r8-flags'].split(' '))
-  if options.r8_flags:
-    args.extend(options.r8_flags.split(' '))
-
-  if inputs:
-    args.extend(inputs)
-
-  if options.dump_args_file:
-    with open(options.dump_args_file, 'w') as args_file:
-      args_file.writelines([arg + os.linesep for arg in args])
-  else:
-    r8.run(args, not options.no_build, not options.no_debug, options.profile,
-           options.track_memory_to_file)
-
-if __name__ == '__main__':
-  sys.exit(main())
diff --git a/tools/test.bat b/tools/test.bat
new file mode 100644
index 0000000..a343096
--- /dev/null
+++ b/tools/test.bat
@@ -0,0 +1,9 @@
+@echo off
+:: 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.
+setlocal
+
+PATH=%~dp0;%PATH%
+
+python "%~dp0\test.py" %*
\ No newline at end of file
diff --git a/tools/test.py b/tools/test.py
index 2f5baf8..49a6f3c 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -7,6 +7,7 @@
 # if an argument is given, run only tests with that pattern. This script will
 # force the tests to run, even if no input changed.
 
+import os
 import gradle
 import optparse
 import sys
@@ -47,6 +48,9 @@
   result.add_option('--jctf_compile_only',
       help="Don't run, only compile JCTF tests.",
       default=False, action='store_true')
+  result.add_option('--disable_assertions',
+      help="Disable assertions when running tests.",
+      default=False, action='store_true')
 
   return result.parse_args()
 
@@ -74,15 +78,23 @@
     gradle_args.append('-Ponly_jctf')
   if options.jctf_compile_only:
     gradle_args.append('-Pjctf_compile_only')
+  if options.disable_assertions:
+    gradle_args.append('-Pdisable_assertions')
   if len(args) > 0:
     gradle_args.append('--tests')
     gradle_args.append(args[0])
-
+  if os.name == 'nt':
+    # temporary hack
+    gradle_args.append('-Pno_internal')
+    gradle_args.append('-x')
+    gradle_args.append('createJctfTests')
+    gradle_args.append('-x')
+    gradle_args.append('jctfCommonJar')
+    gradle_args.append('-x')
+    gradle_args.append('jctfTestsClasses')
   vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
   for art_vm in vms_to_test:
     gradle.RunGradle(gradle_args + ['-Pdex_vm=%s' % art_vm])
 
 if __name__ == '__main__':
   sys.exit(Main())
-
-
diff --git a/tools/test_android_cts.py b/tools/test_android_cts.py
index 384ce70..606611d 100755
--- a/tools/test_android_cts.py
+++ b/tools/test_android_cts.py
@@ -14,11 +14,9 @@
 #   cd build/aosp
 #   repo manifest -o ../../third_party/aosp_manifest.xml -r
 #
-# The baseline is the `test_result.xml` file which is created with an AOSP
-# build which uses the default (JACK) toolset.
-#
-# Use this script, with '--tool=jack' to reproduce the baseline results
-#
+# The baseline is a set of `test_result.xml` files in
+# third_party/android_cts_baseline/jack. The current test considered a success
+# if all tests pass that consistently pass in the baseline.
 
 from __future__ import print_function
 from glob import glob
@@ -30,12 +28,13 @@
 import os
 import re
 import sys
+import time
 
 import gradle
 import utils
 
-CTS_BASELINE = join(utils.REPO_ROOT,
-  'third_party/android_cts_baseline/test_result.xml')
+CTS_BASELINE_FILES_DIR = join(utils.REPO_ROOT,
+  'third_party/android_cts_baseline/jack')
 AOSP_MANIFEST_XML = join(utils.REPO_ROOT, 'third_party',
   'aosp_manifest.xml')
 AOSP_HELPER_SH = join(utils.REPO_ROOT, 'scripts', 'aosp_helper.sh')
@@ -96,97 +95,33 @@
       return False
   return True
 
-# Read the xml test result file into an in-memory tree:
-# Extract only the Module/TestCase/Test names and outcome (True|False for
-# PASS|FAIL):
-#
-#     tree[module_name][testcase_name][test_name] = True|False
-#
-def read_test_result_into_tree(filename):
-  re_module = re.compile('<Module name="([^"]*)"')
-  re_testcase = re.compile('<TestCase name="([^"]*)"')
-  re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
+# Return list of fully qualified names of tests passing in
+# all the files.
+def consistently_passing_tests_from_test_results(filenames):
   tree = {}
   module = None
   testcase = None
-  with open(filename) as f:
-    for line in f:
-      m = re_module.search(line)
-      if m:
-        module_name = m.groups()[0]
-        tree[module_name] = {}
-        module = tree[module_name]
-        continue
+  # Build a tree with leaves True|False|None for passing, failing and flaky
+  # tests.
+  for f in filenames:
+    for x in utils.read_cts_test_result(f):
+      if type(x) is utils.CtsModule:
+        module = tree.setdefault(x.name, {})
+      elif type(x) is utils.CtsTestCase:
+        testcase = module.setdefault(x.name, {})
+      else:
+        outcome = testcase.setdefault(x.name, x.outcome)
+        if outcome is not None and outcome != x.outcome:
+          testcase[x.name] = None
 
-      m = re_testcase.search(line)
-      if m:
-        testcase_name = m.groups()[0]
-        module[testcase_name] = {}
-        testcase = module[testcase_name]
-        continue
+  result = []
+  for module_name, module in tree.iteritems():
+    for test_case_name, test_case in module.iteritems():
+      result.extend(['{}/{}/{}'.format(module_name, test_case_name, test_name)
+          for test_name, test in test_case.iteritems()
+              if test])
 
-      m = re_test.search(line)
-      if m:
-        outcome = m.groups()[0]
-        test_name = m.groups()[1]
-        assert outcome in ["fail", "pass"]
-        testcase[test_name] = outcome == "pass"
-  return tree
-
-# Report the items with the title
-def report_key_diff(title, items, prefix = ''):
-  if len(items) > 0:
-    print(title, ":")
-    for x in items:
-      print("- {}{}".format(prefix, x))
-    print()
-
-
-def diff_sets(base_minus_result_title, result_minus_base_title,
-    base_set, result_set, prefix = ''):
-  base_minus_result = base_set - result_set
-  result_minus_base = result_set - base_set
-  report_key_diff(base_minus_result_title, base_minus_result, prefix)
-  report_key_diff(result_minus_base_title, result_minus_base, prefix)
-  return len(base_minus_result) > 0 or len(result_minus_base) > 0
-
-def diff_tree_report(baseline_tree, result_tree):
-  baseline_modules = set(baseline_tree.keys())
-  result_modules = set(result_tree.keys())
-  differ = diff_sets('Modules missing from current result',
-      'New modules appeared in current result',
-      baseline_modules, result_modules)
-  for module in (result_modules & baseline_modules):
-    baseline_module = baseline_tree[module]
-    result_module = result_tree[module]
-    baseline_testcases = set(baseline_module.keys())
-    result_testcases = set(result_module.keys())
-    differ = diff_sets('Test cases missing from current result',
-        'New test cases appeared in current result',
-        baseline_testcases, result_testcases, module + '/') \
-        or differ
-    for testcase in (result_testcases & baseline_testcases):
-      baseline_testcase = baseline_module[testcase]
-      result_testcase = result_module[testcase]
-      baseline_tests = set(baseline_testcase.keys())
-      result_tests = set(result_testcase.keys())
-      differ = diff_sets('Tests missing from current result',
-          'New tests appeared in current result',
-          baseline_tests, result_tests, module + '/' + testcase + '/') \
-          or differ
-      need_newline_at_end = False
-      for test in (result_tests & baseline_tests):
-        baseline_outcome = baseline_testcase[test]
-        result_outcome = result_testcase[test]
-        if baseline_outcome != result_outcome:
-          differ = True
-          print('Test: {}/{}/{}, change: {}'.format(
-            module, testcase, test,
-            'PASS -> FAIL' if baseline_outcome else 'FAIL -> PASS'))
-          need_newline_at_end = True
-      if need_newline_at_end:
-        print()
-  return differ
+  return result
 
 def setup_and_clean(tool_is_d8, clean_dex):
   # Two output dirs, one for the android image and one for cts tests.
@@ -296,8 +231,6 @@
   re_summary = re.compile('<Summary ')
 
   summaries = [('Summary from current test results: ', results_xml)]
-  if not args.no_baseline:
-    summaries.append(('Summary from baseline: ', CTS_BASELINE))
 
   for (title, result_file) in summaries:
     print(title, result_file)
@@ -312,10 +245,31 @@
   else:
     print('Comparing test results to baseline:\n')
 
-    result_tree = read_test_result_into_tree(results_xml)
-    baseline_tree = read_test_result_into_tree(CTS_BASELINE)
+    passing_tests = consistently_passing_tests_from_test_results([results_xml])
+    baseline_results = \
+        [f for f in glob(join(CTS_BASELINE_FILES_DIR, '*.xml'))]
+    assert len(baseline_results) != 0
 
-    r = EXIT_FAILURE if diff_tree_report(baseline_tree, result_tree) else 0
+    passing_tests_in_baseline = \
+        consistently_passing_tests_from_test_results(baseline_results)
+
+    missing_or_failing_tests = \
+        set(passing_tests_in_baseline) - set(passing_tests)
+
+    num_tests = len(missing_or_failing_tests)
+    if num_tests != 0:
+      if num_tests > 1:
+        text = '{} tests that consistently pass in the baseline' \
+          ' are missing or failing in the current test:'.format(num_tests)
+      else:
+        text = '1 test that consistently passes in the baseline' \
+          ' is missing or failing in the current test:'
+      print(text)
+      for t in missing_or_failing_tests:
+        print(t)
+      r = EXIT_FAILURE
+    else:
+      r = 0
 
   if args.save_result:
     copy2(results_xml, args.save_result)
diff --git a/tools/utils.py b/tools/utils.py
index 67cac46..8c4f710 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -6,6 +6,7 @@
 
 import hashlib
 import os
+import re
 import shutil
 import subprocess
 import sys
@@ -13,7 +14,6 @@
 
 TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
 REPO_ROOT = os.path.realpath(os.path.join(TOOLS_DIR, '..'))
-DOWNLOAD_DEPS = os.path.join(REPO_ROOT, 'scripts', 'download-deps.sh')
 
 def PrintCmd(s):
   if type(s) is list:
@@ -22,8 +22,13 @@
   # I know this will hit os on windows eventually if we don't do this.
   sys.stdout.flush()
 
+def IsWindows():
+  return os.name == 'nt'
+
 def DownloadFromGoogleCloudStorage(sha1_file, bucket='r8-deps'):
-  cmd = ["download_from_google_storage", "-n", "-b", bucket, "-u", "-s",
+  suffix = '.bat' if IsWindows() else ''
+  download_script = 'download_from_google_storage%s' % suffix
+  cmd = [download_script, '-n', '-b', bucket, '-u', '-s',
          sha1_file]
   PrintCmd(cmd)
   subprocess.check_call(cmd)
@@ -63,9 +68,46 @@
 
  def __enter__(self):
    self._old_cwd = os.getcwd()
-   print "Enter directory = ", self._working_directory
+   print 'Enter directory = ', self._working_directory
    os.chdir(self._working_directory)
 
  def __exit__(self, *_):
-   print "Enter directory = ", self._old_cwd
+   print 'Enter directory = ', self._old_cwd
    os.chdir(self._old_cwd)
+
+# Reading Android CTS test_result.xml
+
+class CtsModule(object):
+  def __init__(self, module_name):
+    self.name = module_name
+
+class CtsTestCase(object):
+  def __init__(self, test_case_name):
+    self.name = test_case_name
+
+class CtsTest(object):
+  def __init__(self, test_name, outcome):
+    self.name = test_name
+    self.outcome = outcome
+
+# Generator yielding CtsModule, CtsTestCase or CtsTest from
+# reading through a CTS test_result.xml file.
+def read_cts_test_result(file_xml):
+  re_module = re.compile('<Module name="([^"]*)"')
+  re_test_case = re.compile('<TestCase name="([^"]*)"')
+  re_test = re.compile('<Test result="(pass|fail)" name="([^"]*)"')
+  with open(file_xml) as f:
+    for line in f:
+      m = re_module.search(line)
+      if m:
+        yield CtsModule(m.groups()[0])
+        continue
+      m = re_test_case.search(line)
+      if m:
+        yield CtsTestCase(m.groups()[0])
+        continue
+      m = re_test.search(line)
+      if m:
+        outcome = m.groups()[0]
+        assert outcome in ['fail', 'pass']
+        yield CtsTest(m.groups()[1], outcome == 'pass')
diff --git a/tools/windows/README.dx b/tools/windows/README.dx
new file mode 100644
index 0000000..348dc4a
--- /dev/null
+++ b/tools/windows/README.dx
@@ -0,0 +1,25 @@
+Dx version: 1.12
+dx.bat and dx.jar are fetched from SDK build tools 24.0.0.
+dx.bat has been modified so that the code that checks if 'java' if on current path
+is removed and replaced by a direct reference to it.
+This is done because this relies on a tool found in the SDK and not present in the
+current package.
+Diff:
+
+26,29c26,29
+< set java_exe=
+< if exist    "%~dp0..\tools\lib\find_java.bat" call    "%~dp0..\tools\lib\find_java.bat"
+< if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"
+< if not defined java_exe goto :EOF
+---
+> REM set java_exe=
+> REM if exist    "%~dp0..\tools\lib\find_java.bat" call    "%~dp0..\tools\lib\find_java.bat"
+> REM if exist "%~dp0..\..\tools\lib\find_java.bat" call "%~dp0..\..\tools\lib\find_java.bat"
+> REM if not defined java_exe goto :EOF
+87c87
+< call "%java_exe%" %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
+---
+> call java %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
+
+dexmerger.bat has been copied from dx.bat, and the command line has been updated
+according to the SDK dexmerger bash script to call the right class.
\ No newline at end of file
diff --git a/tools/windows/dx.tar.gz.sha1 b/tools/windows/dx.tar.gz.sha1
new file mode 100644
index 0000000..0f3e345
--- /dev/null
+++ b/tools/windows/dx.tar.gz.sha1
@@ -0,0 +1 @@
+9adeae753e17fa0a663e4d458b406a39ded27621
\ No newline at end of file
diff --git a/tools/youtube_data.py b/tools/youtube_data.py
index 0fb39e3..b32a372 100644
--- a/tools/youtube_data.py
+++ b/tools/youtube_data.py
@@ -9,37 +9,20 @@
 THIRD_PARTY = os.path.join(utils.REPO_ROOT, 'third_party')
 BASE = os.path.join(THIRD_PARTY, 'youtube')
 
-V11_47_BASE = os.path.join(BASE, 'youtube.android_11.47')
-V11_47_PREFIX = os.path.join(V11_47_BASE, 'YouTubeRelease')
-
 V12_10_BASE = os.path.join(BASE, 'youtube.android_12.10')
 V12_10_PREFIX = os.path.join(V12_10_BASE, 'YouTubeRelease')
 
 V12_17_BASE = os.path.join(BASE, 'youtube.android_12.17')
 V12_17_PREFIX = os.path.join(V12_17_BASE, 'YouTubeRelease')
 
+V12_22_BASE = os.path.join(BASE, 'youtube.android_12.22')
+V12_22_PREFIX = os.path.join(V12_22_BASE, 'YouTubeRelease')
+
 # NOTE: we always use android.jar for SDK v25, later we might want to revise it
 #       to use proper android.jar version for each of youtube version separately.
 ANDROID_JAR = os.path.join(THIRD_PARTY, 'android_jar', 'lib-v25', 'android.jar')
 
 VERSIONS = {
-  '11.47': {
-    'dex' : {
-      'inputs': [os.path.join(V11_47_BASE, 'YouTubeRelease_unsigned.apk')],
-      'pgmap': '%s_proguard.map' % V11_47_PREFIX,
-      'libraries' : [ANDROID_JAR],
-      'r8-flags': '--ignore-missing-classes',
-    },
-    'deploy' : {
-      'inputs': ['%s_deploy.jar' % V11_47_PREFIX],
-      'pgconf': ['%s_proguard.config' % V11_47_PREFIX,
-                 '%s/proguardsettings/YouTubeRelease_proguard.config' % THIRD_PARTY],
-    },
-    'proguarded' : {
-      'inputs': ['%s_proguard.jar' % V11_47_PREFIX],
-      'pgmap': '%s_proguard.map' % V11_47_PREFIX
-    }
-  },
   '12.10': {
     'dex' : {
       'inputs': [os.path.join(V12_10_BASE, 'YouTubeRelease_unsigned.apk')],
@@ -74,4 +57,26 @@
       'pgmap': '%s_proguard.map' % V12_17_PREFIX
     }
   },
+  '12.22': {
+    'dex' : {
+      'inputs': [os.path.join(V12_22_BASE, 'YouTubeRelease_unsigned.apk')],
+      'pgmap': '%s_proguard.map' % V12_22_PREFIX,
+      'libraries' : [ANDROID_JAR],
+      'r8-flags': '--ignore-missing-classes',
+    },
+    'deploy' : {
+      'inputs': ['%s_deploy.jar' % V12_22_PREFIX],
+      'pgconf': [
+          '%s_proguard.config' % V12_22_PREFIX,
+          '%s/proguardsettings/YouTubeRelease_proguard.config' % THIRD_PARTY],
+      'multidexrules' : [
+          os.path.join(V12_22_BASE, 'mainDexClasses.rules'),
+          os.path.join(V12_22_BASE, 'main-dex-classes-release.cfg'),
+          os.path.join(V12_22_BASE, 'main_dex_YouTubeRelease_proguard.cfg')],
+    },
+    'proguarded' : {
+      'inputs': ['%s_proguard.jar' % V12_22_PREFIX],
+      'pgmap': '%s_proguard.map' % V12_22_PREFIX
+    }
+  },
 }