Merge "Show desktop notifications when tests complete."
diff --git a/build.gradle b/build.gradle
index 63513ce..82feb00 100644
--- a/build.gradle
+++ b/build.gradle
@@ -435,6 +435,20 @@
     }
 }
 
+task maindex(type: Jar) {
+    from sourceSets.main.output
+    baseName 'maindex'
+    manifest {
+      attributes 'Main-Class': 'com.android.tools.r8.GenerateMainDexList'
+    }
+    // In order to build without dependencies, pass the exclude_deps property using:
+    // gradle -Pexclude_deps maindex
+    if (!project.hasProperty('exclude_deps')) {
+        // Also include dependencies
+        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
+    }
+}
+
 task ExtractMarker(type: Jar) {
     from sourceSets.main.output
     baseName 'extractmarker'
@@ -565,6 +579,7 @@
             executable file("third_party/kotlin/kotlinc/bin/kotlinc");
         }
         args "-include-runtime"
+        args "-nowarn"
         args "-d"
         args "build/test/${kotlinHostJar}"
         args fileTree(dir: kotlinResourcesDir, include: '**/*.kt')
diff --git a/src/main/java/com/android/tools/r8/ApiLevelException.java b/src/main/java/com/android/tools/r8/ApiLevelException.java
index ec9fe68..14a530c 100644
--- a/src/main/java/com/android/tools/r8/ApiLevelException.java
+++ b/src/main/java/com/android/tools/r8/ApiLevelException.java
@@ -3,27 +3,28 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import com.android.tools.r8.utils.AndroidApiLevel;
+
 /**
  * Exception to signal features that are not supported until a given API level.
  */
 public class ApiLevelException extends CompilationException {
 
   public ApiLevelException(
-      int minApiLevel, String minApiLevelString, String unsupportedFeatures, String sourceString) {
-    super(makeMessage(minApiLevel, minApiLevelString, unsupportedFeatures, sourceString));
-    assert minApiLevel > 0;
-    assert minApiLevelString != null;
+      AndroidApiLevel minApiLevel, String unsupportedFeatures, String sourceString) {
+    super(makeMessage(minApiLevel, unsupportedFeatures, sourceString));
+    assert minApiLevel != null;
     assert unsupportedFeatures != null;
   }
 
   private static String makeMessage(
-      int minApiLevel, String minApiLevelString, String unsupportedFeatures, String sourceString) {
+      AndroidApiLevel minApiLevel, String unsupportedFeatures, String sourceString) {
     String message =
         unsupportedFeatures
             + " are only supported starting with "
-            + minApiLevelString
+            + minApiLevel.getName()
             + " (--min-api "
-            + minApiLevel
+            + minApiLevel.getLevel()
             + ")";
     message = (sourceString != null) ? message + ": " + sourceString : message;
     return message;
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 9ddbd9e..cbd8dd6 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -3,51 +3,33 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.utils.AndroidApp;
-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;
 
+/**
+ * Base class for commands and command builders for applications/tools which take an Android
+ * application (and a main-dex list) as input.
+ */
 abstract class BaseCommand {
 
   private final boolean printHelp;
   private final boolean printVersion;
 
   private final AndroidApp app;
-  private final Path outputPath;
-  private final OutputMode outputMode;
-  private final CompilationMode mode;
-  private final int minApiLevel;
 
   BaseCommand(boolean printHelp, boolean printVersion) {
     this.printHelp = printHelp;
     this.printVersion = printVersion;
     // All other fields are initialized with stub/invalid values.
     this.app = null;
-    this.outputPath = null;
-    this.outputMode = OutputMode.Indexed;
-    this.mode = null;
-    this.minApiLevel = 0;
   }
 
-  BaseCommand(
-      AndroidApp app,
-      Path outputPath,
-      OutputMode outputMode,
-      CompilationMode mode,
-      int minApiLevel) {
+  BaseCommand(AndroidApp app) {
     assert app != null;
-    assert mode != null;
-    assert minApiLevel > 0;
     this.app = app;
-    this.outputPath = outputPath;
-    this.outputMode = outputMode;
-    this.mode = mode;
-    this.minApiLevel = minApiLevel;
     // Print options are not set.
     printHelp = false;
     printVersion = false;
@@ -69,52 +51,27 @@
   // Internal access to the internal options.
   abstract InternalOptions getInternalOptions();
 
-  public Path getOutputPath() {
-    return outputPath;
-  }
-
-  public CompilationMode getMode() {
-    return mode;
-  }
-
-  public int getMinApiLevel() {
-    return minApiLevel;
-  }
-
-  public OutputMode getOutputMode() {
-    return outputMode;
-  }
-
-  abstract static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
+  abstract public static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
 
     private boolean printHelp = false;
     private boolean printVersion = false;
     private final AndroidApp.Builder app;
-    private Path outputPath = null;
-    private OutputMode outputMode = OutputMode.Indexed;
-    private CompilationMode mode;
-    private int minApiLevel = Constants.DEFAULT_ANDROID_API;
 
-    // Internal flag used by CompatDx to ignore dex files in archives.
-    protected boolean ignoreDexInArchive = false;
-
-    protected Builder(CompilationMode mode) {
-      this(AndroidApp.builder(), mode, false);
+    protected Builder() {
+      this(AndroidApp.builder(), false);
     }
 
-    protected Builder(CompilationMode mode, boolean ignoreDexInArchive) {
-      this(AndroidApp.builder(), mode, ignoreDexInArchive);
+    protected Builder(boolean ignoreDexInArchive) {
+      this(AndroidApp.builder(), ignoreDexInArchive);
     }
 
     // Internal constructor for testing.
     Builder(AndroidApp app, CompilationMode mode) {
-      this(AndroidApp.builder(app), mode, false);
+      this(AndroidApp.builder(app), false);
     }
 
-    private Builder(AndroidApp.Builder builder, CompilationMode mode, boolean ignoreDexInArchive) {
-      assert mode != null;
+    protected Builder(AndroidApp.Builder builder, boolean ignoreDexInArchive) {
       this.app = builder;
-      this.mode = mode;
       app.setIgnoreDexInArchive(ignoreDexInArchive);
     }
 
@@ -181,52 +138,6 @@
       return self();
     }
 
-    /** Get current compilation mode. */
-    public CompilationMode getMode() {
-      return mode;
-    }
-
-    /** Set compilation mode. */
-    public B setMode(CompilationMode mode) {
-      assert mode != null;
-      this.mode = mode;
-      return self();
-    }
-
-    /** Get the output path. Null if not set. */
-    public Path getOutputPath() {
-      return outputPath;
-    }
-
-    /** Get the output mode. */
-    public OutputMode getOutputMode() {
-      return outputMode;
-    }
-
-    /** Set an output path. Must be an existing directory or a zip file. */
-    public B setOutputPath(Path outputPath) {
-      this.outputPath = outputPath;
-      return self();
-    }
-
-    /** Set an output mode. */
-    public B setOutputMode(OutputMode outputMode) {
-      this.outputMode = outputMode;
-      return self();
-    }
-
-    /** Get the minimum API level (aka SDK version). */
-    public int getMinApiLevel() {
-      return minApiLevel;
-    }
-
-    /** Set the minimum required API level (aka SDK version). */
-    public B setMinApiLevel(int minApiLevel) {
-      assert minApiLevel > 0;
-      this.minApiLevel = minApiLevel;
-      return self();
-    }
-
     /**
      * Add main-dex list files.
      *
@@ -300,11 +211,7 @@
     }
 
     protected void validate() throws CompilationException {
-      if (app.hasMainDexList() && outputMode == OutputMode.FilePerClass) {
-        throw new CompilationException(
-            "Option --main-dex-list cannot be used with --file-per-class");
-      }
-      FileUtils.validateOutputFile(outputPath);
+      // Currently does nothing.
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
new file mode 100644
index 0000000..5e8f975
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -0,0 +1,146 @@
+// 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.dex.Constants;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.OutputMode;
+import java.nio.file.Path;
+
+/**
+ * Base class for commands and command builders for compiler applications/tools which besides an
+ * Android application (and a main-dex list) also takes compilation output, compilation mode and
+ * min API level as input.
+ */
+abstract class BaseCompilerCommand extends BaseCommand {
+
+  private final Path outputPath;
+  private final OutputMode outputMode;
+  private final CompilationMode mode;
+  private final int minApiLevel;
+
+  BaseCompilerCommand(boolean printHelp, boolean printVersion) {
+    super(printHelp, printVersion);
+
+    this.outputPath = null;
+    this.outputMode = OutputMode.Indexed;
+    this.mode = null;
+    this.minApiLevel = 0;
+  }
+
+  BaseCompilerCommand(
+      AndroidApp app,
+      Path outputPath,
+      OutputMode outputMode,
+      CompilationMode mode,
+      int minApiLevel) {
+    super(app);
+    assert mode != null;
+    assert minApiLevel > 0;
+    this.outputPath = outputPath;
+    this.outputMode = outputMode;
+    this.mode = mode;
+    this.minApiLevel = minApiLevel;
+  }
+
+  public Path getOutputPath() {
+    return outputPath;
+  }
+
+  public CompilationMode getMode() {
+    return mode;
+  }
+
+  public int getMinApiLevel() {
+    return minApiLevel;
+  }
+
+  public OutputMode getOutputMode() {
+    return outputMode;
+  }
+
+  abstract public static class Builder<C extends BaseCompilerCommand, B extends Builder<C, B>>
+      extends BaseCommand.Builder<C, B> {
+
+    private Path outputPath = null;
+    private OutputMode outputMode = OutputMode.Indexed;
+    private CompilationMode mode;
+    private int minApiLevel = Constants.DEFAULT_ANDROID_API;
+
+    protected Builder(CompilationMode mode) {
+      this(AndroidApp.builder(), mode, false);
+    }
+
+    protected Builder(CompilationMode mode, boolean ignoreDexInArchive) {
+      this(AndroidApp.builder(), mode, ignoreDexInArchive);
+    }
+
+    // Internal constructor for testing.
+    Builder(AndroidApp app, CompilationMode mode) {
+      this(AndroidApp.builder(app), mode, false);
+    }
+
+    private Builder(AndroidApp.Builder builder, CompilationMode mode, boolean ignoreDexInArchive) {
+      super(builder, ignoreDexInArchive);
+      assert mode != null;
+      this.mode = mode;
+    }
+
+    /** Get current compilation mode. */
+    public CompilationMode getMode() {
+      return mode;
+    }
+
+    /** Set compilation mode. */
+    public B setMode(CompilationMode mode) {
+      assert mode != null;
+      this.mode = mode;
+      return self();
+    }
+
+    /** Get the output path. Null if not set. */
+    public Path getOutputPath() {
+      return outputPath;
+    }
+
+    /** Get the output mode. */
+    public OutputMode getOutputMode() {
+      return outputMode;
+    }
+
+    /** Set an output path. Must be an existing directory or a zip file. */
+    public B setOutputPath(Path outputPath) {
+      this.outputPath = outputPath;
+      return self();
+    }
+
+    /** Set an output mode. */
+    public B setOutputMode(OutputMode outputMode) {
+      this.outputMode = outputMode;
+      return self();
+    }
+
+    /** Get the minimum API level (aka SDK version). */
+    public int getMinApiLevel() {
+      return minApiLevel;
+    }
+
+    /** Set the minimum required API level (aka SDK version). */
+    public B setMinApiLevel(int minApiLevel) {
+      assert minApiLevel > 0;
+      this.minApiLevel = minApiLevel;
+      return self();
+    }
+
+    protected void validate() throws CompilationException {
+      super.validate();
+      if (getAppBuilder().hasMainDexList() && outputMode == OutputMode.FilePerClass) {
+        throw new CompilationException(
+            "Option --main-dex-list cannot be used with --file-per-class");
+      }
+      FileUtils.validateOutputFile(outputPath);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/CompilationException.java b/src/main/java/com/android/tools/r8/CompilationException.java
index 4fd3654..8e1e56c 100644
--- a/src/main/java/com/android/tools/r8/CompilationException.java
+++ b/src/main/java/com/android/tools/r8/CompilationException.java
@@ -36,5 +36,17 @@
   public CompilationException(Throwable cause) {
     super(cause.getMessage(), cause);
   }
+
+  protected CompilationException() {
+    super();
+  }
+
+  public String getMessageForD8() {
+    return super.getMessage();
+  }
+
+  public String getMessageForR8() {
+    return super.getMessage();
+  }
 }
 
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 5321808..6598ae4 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.MainDexError;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.ir.conversion.IRConverter;
@@ -118,7 +118,7 @@
   }
 
   /** Command-line entry to D8. */
-  public static void main(String[] args) throws IOException {
+  public static void main(String[] args) {
     if (args.length == 0) {
       System.err.println(USAGE_MESSAGE);
       System.exit(STATUS_ERROR);
@@ -140,8 +140,7 @@
       cause.printStackTrace();
       System.exit(STATUS_ERROR);
     } catch (CompilationException e) {
-      System.err.println("Compilation failed: " + e.getMessage());
-      System.err.println(USAGE_MESSAGE);
+      System.err.println("Compilation failed: " + e.getMessageForD8());
       System.exit(STATUS_ERROR);
     }
   }
@@ -199,8 +198,6 @@
 
       options.printWarnings();
       return output;
-    } catch (MainDexError mainDexError) {
-      throw new CompilationError(mainDexError.getMessageForD8());
     } catch (ExecutionException e) {
       R8.unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 3618ab5..5b2e5cf 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -27,12 +27,12 @@
  *     .build();
  * </pre>
  */
-public class D8Command extends BaseCommand {
+public class D8Command extends BaseCompilerCommand {
 
   /**
    * Builder for constructing a D8Command.
    */
-  public static class Builder extends BaseCommand.Builder<D8Command, Builder> {
+  public static class Builder extends BaseCompilerCommand.Builder<D8Command, Builder> {
 
     private boolean intermediate = false;
 
diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java
index 3827cae..c27deb5 100644
--- a/src/main/java/com/android/tools/r8/DexSegments.java
+++ b/src/main/java/com/android/tools/r8/DexSegments.java
@@ -5,18 +5,14 @@
 
 import com.android.tools.r8.dex.DexFileReader;
 import com.android.tools.r8.dex.Segment;
-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 com.google.common.io.Closer;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ExecutionException;
 
 public class DexSegments {
   private static class Command extends BaseCommand {
@@ -24,10 +20,6 @@
     public static class Builder
         extends BaseCommand.Builder<Command, Builder> {
 
-      private Builder() {
-        super(CompilationMode.RELEASE);
-      }
-
       @Override
       Command.Builder self() {
         return this;
@@ -40,8 +32,7 @@
           return new Command(isPrintHelp());
         }
         validate();
-        return new Command(
-            getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
+        return new Command(getAppBuilder().build());
       }
     }
 
@@ -79,13 +70,8 @@
       }
     }
 
-    private Command(
-        AndroidApp inputApp,
-        Path outputPath,
-        OutputMode outputMode,
-        CompilationMode mode,
-        int minApiLevel) {
-      super(inputApp, outputPath, outputMode, mode, minApiLevel);
+    private Command(AndroidApp inputApp) {
+      super(inputApp);
     }
 
     private Command(boolean printHelp) {
@@ -99,7 +85,7 @@
   }
 
   public static void main(String[] args)
-      throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
+      throws IOException, CompilationException {
     Command.Builder builder = Command.parse(args);
     Command command = builder.build();
     if (command.isPrintHelp()) {
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 2f1d523..bb72d5d 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -3,10 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 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;
@@ -16,13 +14,13 @@
 public class Disassemble {
   public static class DisassembleCommand extends BaseCommand {
 
+    private final Path outputPath;
+
     public static class Builder
         extends BaseCommand.Builder<DisassembleCommand, DisassembleCommand.Builder> {
-      private boolean useSmali = false;
 
-      private Builder() {
-        super(CompilationMode.RELEASE);
-      }
+      private Path outputPath = null;
+      private boolean useSmali = false;
 
       @Override
       DisassembleCommand.Builder self() {
@@ -34,6 +32,15 @@
         return this;
       }
 
+      public Path getOutputPath() {
+        return outputPath;
+      }
+
+      public DisassembleCommand.Builder setOutputPath(Path outputPath) {
+        this.outputPath = outputPath;
+        return this;
+      }
+
       public DisassembleCommand.Builder setUseSmali(boolean useSmali) {
         this.useSmali = useSmali;
         return this;
@@ -47,13 +54,7 @@
         }
 
         validate();
-        return new DisassembleCommand(
-            getAppBuilder().build(),
-            getOutputPath(),
-            getOutputMode(),
-            getMode(),
-            getMinApiLevel(),
-            useSmali);
+        return new DisassembleCommand(getAppBuilder().build(), getOutputPath(), useSmali);
       }
     }
 
@@ -94,6 +95,9 @@
           builder.setUseSmali(true);
         } else if (arg.equals("--pg-map")) {
           builder.setProguardMapFile(Paths.get(args[++i]));
+        } else if (arg.equals("--output")) {
+          String outputPath = args[++i];
+          builder.setOutputPath(Paths.get(outputPath));
         } else {
           if (arg.startsWith("--")) {
             throw new CompilationException("Unknown option: " + arg);
@@ -103,23 +107,22 @@
       }
     }
 
-    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";
+    private DisassembleCommand(AndroidApp inputApp, Path outputPath, boolean useSmali) {
+      super(inputApp);
+      this.outputPath = outputPath;
       this.useSmali = useSmali;
     }
 
     private DisassembleCommand(boolean printHelp, boolean printVersion) {
       super(printHelp, printVersion);
+      this.outputPath = null;
       this.useSmali = false;
     }
 
+    public Path getOutputPath() {
+      return outputPath;
+    }
+
     public boolean useSmali() {
       return useSmali;
     }
@@ -133,7 +136,7 @@
   }
 
   public static void main(String[] args)
-      throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
+      throws IOException, CompilationException, ExecutionException {
     DisassembleCommand.Builder builder = DisassembleCommand.parse(args);
     DisassembleCommand command = builder.build();
     if (command.isPrintHelp()) {
diff --git a/src/main/java/com/android/tools/r8/ExtractMarker.java b/src/main/java/com/android/tools/r8/ExtractMarker.java
index c1234b8..d798781 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarker.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarker.java
@@ -3,19 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import com.google.common.collect.ImmutableList;
-
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.graph.DexApplication;
-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.android.tools.r8.utils.Timing;
-
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.concurrent.ExecutionException;
 
@@ -25,10 +20,6 @@
     public static class Builder
         extends BaseCommand.Builder<ExtractMarker.Command, ExtractMarker.Command.Builder> {
 
-      private Builder() {
-        super(CompilationMode.RELEASE);
-      }
-
       @Override
       ExtractMarker.Command.Builder self() {
         return this;
@@ -41,8 +32,7 @@
           return new ExtractMarker.Command(isPrintHelp());
         }
         validate();
-        return new ExtractMarker.Command(
-            getAppBuilder().build(), getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
+        return new ExtractMarker.Command(getAppBuilder().build());
       }
     }
 
@@ -80,13 +70,8 @@
       }
     }
 
-    private Command(
-        AndroidApp inputApp,
-        Path outputPath,
-        OutputMode outputMode,
-        CompilationMode mode,
-        int minApiLevel) {
-      super(inputApp, outputPath, outputMode, mode, minApiLevel);
+    private Command(AndroidApp inputApp) {
+      super(inputApp);
     }
 
     private Command(boolean printHelp) {
@@ -100,7 +85,7 @@
   }
 
   public static void main(String[] args)
-      throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
+      throws IOException, CompilationException, ExecutionException {
     ExtractMarker.Command.Builder builder = ExtractMarker.Command.parse(args);
     ExtractMarker.Command command = builder.build();
     if (command.isPrintHelp()) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
new file mode 100644
index 0000000..44d523e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.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.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.MainDexListBuilder;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+public class GenerateMainDexList {
+  private static final String VERSION = "v0.2.0";
+  private final Timing timing = new Timing("maindex");
+  private final InternalOptions options;
+
+  private GenerateMainDexList(InternalOptions options) {
+    this.options = options;
+  }
+
+  private List<String> run(AndroidApp app) throws IOException, ExecutionException {
+    ExecutorService executor = ThreadUtils.getExecutorService(options);
+    DexApplication application = new ApplicationReader(app, options, timing).read(executor);
+    AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+    RootSet mainDexRootSet =
+        new RootSetBuilder(application, appInfo, options.mainDexKeepRules).run(executor);
+    Set<DexType> mainDexBaseClasses = new Enqueuer(appInfo).traceMainDex(mainDexRootSet, timing);
+    Set<DexType> mainDexClasses = new MainDexListBuilder(mainDexBaseClasses, application).run();
+
+    List<String> result = mainDexClasses.stream()
+        .map(c -> c.toSourceString().replace('.', '/') + ".class")
+        .sorted()
+        .collect(Collectors.toList());
+
+    if (options.printMainDexListFile != null) {
+      try (OutputStream mainDexOut = Files.newOutputStream(options.printMainDexListFile,
+          StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
+        PrintWriter writer = new PrintWriter(mainDexOut);
+        result.forEach(writer::println);
+        writer.flush();
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Main API entry for computing the main-dex list.
+   *
+   * The main-dex list is represented as a list of strings, each string specifies one class to
+   * keep in the primary dex file (<code>classes.dex</code>).
+   *
+   * A class is specified using the following format: "com/example/MyClass.class". That is
+   * "/" as separator between package components, and a trailing ".class".
+   *
+   * @param command main dex-list generator command.
+   * @return classes to keep in the primary dex file.
+   */
+  public static List<String> run(GenerateMainDexListCommand command)
+      throws IOException, ExecutionException {
+    ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+    try {
+      return run(command, executorService);
+    } finally {
+      executorService.shutdown();
+    }
+  }
+
+  /**
+   * Main API entry for computing the main-dex list.
+   *
+   * The main-dex list is represented as a list of strings, each string specifies one class to
+   * keep in the primary dex file (<code>classes.dex</code>).
+   *
+   * A class is specified using the following format: "com/example/MyClass.class". That is
+   * "/" as separator between package components, and a trailing ".class".
+   *
+   * @param command main dex-list generator command.
+   * @param executor executor service from which to get threads for multi-threaded processing.
+   * @return classes to keep in the primary dex file.
+   */
+  public static List<String> run(GenerateMainDexListCommand command, ExecutorService executor)
+      throws IOException, ExecutionException {
+    AndroidApp app = command.getInputApp();
+    InternalOptions options = command.getInternalOptions();
+    return new GenerateMainDexList(options).run(app);
+  }
+
+  public static void main(String[] args)
+      throws IOException, ProguardRuleParserException, CompilationException, ExecutionException {
+    GenerateMainDexListCommand.Builder builder = GenerateMainDexListCommand.parse(args);
+    GenerateMainDexListCommand command = builder.build();
+    if (command.isPrintHelp()) {
+      System.out.println(GenerateMainDexListCommand.USAGE_MESSAGE);
+      return;
+    }
+    if (command.isPrintVersion()) {
+      System.out.println("MainDexListGenerator " + VERSION);
+      return;
+    }
+    List<String> result = run(command);
+    if (command.getMainDexListOutputPath() == null) {
+      result.forEach(System.out::println);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
new file mode 100644
index 0000000..76d7d56
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexListCommand.java
@@ -0,0 +1,200 @@
+// 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.graph.DexItemFactory;
+import com.android.tools.r8.shaking.ProguardConfigurationParser;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GenerateMainDexListCommand extends BaseCommand {
+
+  private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+  private final Path mainDexListOutput;
+  private final DexItemFactory factory;
+
+  /**
+   * Get the output path for the main-dex list. Null if not set.
+   */
+  public Path getMainDexListOutputPath() {
+    return mainDexListOutput;
+  }
+
+  public static class Builder extends BaseCommand.Builder<GenerateMainDexListCommand, Builder> {
+
+    private final DexItemFactory factory = new DexItemFactory();
+    private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
+    private Path mainDexListOutput = null;
+
+    @Override
+    GenerateMainDexListCommand.Builder self() {
+      return this;
+    }
+
+    /**
+     * Add proguard configuration file resources for automatic main dex list calculation.
+     */
+    public GenerateMainDexListCommand.Builder addMainDexRulesFiles(Path... paths) {
+      for (Path path : paths) {
+        mainDexRules.add(new ProguardConfigurationSourceFile(path));
+      }
+      return self();
+    }
+
+    /**
+     * Add proguard configuration file resources for automatic main dex list calculation.
+     */
+    public GenerateMainDexListCommand.Builder addMainDexRulesFiles(List<Path> paths) {
+      for (Path path : paths) {
+        mainDexRules.add(new ProguardConfigurationSourceFile(path));
+      }
+      return self();
+    }
+
+    /**
+     * Add proguard configuration for automatic main dex list calculation.
+     */
+    public GenerateMainDexListCommand.Builder addMainDexRules(List<String> lines) {
+      mainDexRules.add(new ProguardConfigurationSourceStrings(lines));
+      return self();
+    }
+
+    /**
+     * Get the output path for the main-dex list. Null if not set.
+     */
+    public Path getMainDexListOutputPath() {
+      return mainDexListOutput;
+    }
+
+    /**
+     * Set the output file for the main-dex list.
+     *
+     * If the file exists it will be overwritten.
+     */
+    public GenerateMainDexListCommand.Builder setMainDexListOutputPath(Path mainDexListOutputPath) {
+      mainDexListOutput = mainDexListOutputPath;
+      return self();
+    }
+
+
+    @Override
+    public GenerateMainDexListCommand build() throws CompilationException, IOException {
+      // If printing versions ignore everything else.
+      if (isPrintHelp() || isPrintVersion()) {
+        return new GenerateMainDexListCommand(isPrintHelp(), isPrintVersion());
+      }
+
+      validate();
+      ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
+      if (this.mainDexRules.isEmpty()) {
+        mainDexKeepRules = ImmutableList.of();
+      } else {
+        ProguardConfigurationParser parser = new ProguardConfigurationParser(factory);
+        try {
+          parser.parse(mainDexRules);
+        } catch (ProguardRuleParserException e) {
+          throw new CompilationException(e.getMessage(), e.getCause());
+        }
+        mainDexKeepRules = parser.getConfig().getRules();
+      }
+
+      return new GenerateMainDexListCommand(
+          factory,
+          getAppBuilder().build(),
+          mainDexKeepRules,
+          mainDexListOutput);
+    }
+  }
+
+  static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
+      "Usage: maindex [options] <input-files>",
+      " where <input-files> are JAR files",
+      " and options are:",
+      "  --main-dex-rules <file>  # Proguard keep rules for classes to place in the",
+      "                           # primary dex file.",
+      "  --main-dex-list <file>   # List of classes to place in the primary dex file.",
+      "  --main-dex-list-output <file>  # Output the full main-dex list in <file>.",
+      "  --version                # Print the version.",
+      "  --help                   # Print this message."));
+
+
+  public static GenerateMainDexListCommand.Builder builder() {
+    return new GenerateMainDexListCommand.Builder();
+  }
+
+  public static GenerateMainDexListCommand.Builder parse(String[] args)
+      throws CompilationException, IOException {
+    GenerateMainDexListCommand.Builder builder = builder();
+    parse(args, builder);
+    return builder;
+  }
+
+  private static void parse(String[] args, GenerateMainDexListCommand.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("--main-dex-rules")) {
+        builder.addMainDexRulesFiles(Paths.get(args[++i]));
+      } else if (arg.equals("--main-dex-list")) {
+        builder.addMainDexListFiles(Paths.get(args[++i]));
+      } else if (arg.equals("--main-dex-list-output")) {
+        builder.setMainDexListOutputPath(Paths.get(args[++i]));
+      } else {
+        if (arg.startsWith("--")) {
+          throw new CompilationException("Unknown option: " + arg);
+        }
+        builder.addProgramFiles(Paths.get(arg));
+      }
+    }
+  }
+
+  private GenerateMainDexListCommand(
+      DexItemFactory factory,
+      AndroidApp inputApp,
+      ImmutableList<ProguardConfigurationRule> mainDexKeepRules,
+      Path mainDexListOutput) {
+    super(inputApp);
+    this.factory = factory;
+    this.mainDexKeepRules = mainDexKeepRules;
+    this.mainDexListOutput = mainDexListOutput;
+  }
+
+  private GenerateMainDexListCommand(boolean printHelp, boolean printVersion) {
+    super(printHelp, printVersion);
+    this.factory = new DexItemFactory();
+    this.mainDexKeepRules = ImmutableList.of();
+    this.mainDexListOutput = null;
+  }
+
+  @Override
+  InternalOptions getInternalOptions() {
+    InternalOptions internal = new InternalOptions(factory);
+    internal.mainDexKeepRules = mainDexKeepRules;
+    if (mainDexListOutput != null) {
+      internal.printMainDexListFile = mainDexListOutput;
+    }
+    internal.minimalMainDex = internal.debug;
+    internal.removeSwitchMaps = false;
+    internal.inlineAccessors = false;
+    return internal;
+  }
+}
+
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 328f215..b4c19ea 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -7,11 +7,10 @@
 
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.dex.ApplicationWriter;
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.MainDexError;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.ClassAndMemberPublicizer;
@@ -41,6 +40,7 @@
 import com.android.tools.r8.shaking.SimpleClassMerger;
 import com.android.tools.r8.shaking.TreePruner;
 import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.FileUtils;
@@ -99,7 +99,7 @@
       byte[] proguardSeedsData,
       PackageDistribution packageDistribution,
       InternalOptions options)
-      throws ExecutionException {
+      throws ExecutionException, DexOverflowException {
     try {
       Marker marker = getMarker(options);
       return new ApplicationWriter(
@@ -207,7 +207,7 @@
   }
 
   static CompilationResult runForTesting(AndroidApp app, InternalOptions options)
-      throws ProguardRuleParserException, IOException, CompilationException {
+      throws IOException, CompilationException {
     ExecutorService executor = ThreadUtils.getExecutorService(options);
     try {
       return runForTesting(app, options, executor);
@@ -220,20 +220,21 @@
       AndroidApp app,
       InternalOptions options,
       ExecutorService executor)
-      throws ProguardRuleParserException, IOException, CompilationException {
+      throws IOException, CompilationException {
     return new R8(options).run(app, executor);
   }
 
   private CompilationResult run(AndroidApp inputApp, ExecutorService executorService)
-      throws IOException, ProguardRuleParserException, CompilationException {
+      throws IOException, CompilationException {
     if (options.quiet) {
       System.setOut(new PrintStream(ByteStreams.nullOutputStream()));
     }
     try {
-      if (options.minApiLevel >= Constants.ANDROID_O_API
+      AndroidApiLevel oLevel = AndroidApiLevel.O;
+      if (options.minApiLevel >= oLevel.getLevel()
           && !options.mainDexKeepRules.isEmpty()) {
-        throw new CompilationError("Automatic main dex list is not supported when compiling for"
-            + " android O and later (--min-api " + Constants.ANDROID_O_API + ")");
+        throw new CompilationError("Automatic main dex list is not supported when compiling for "
+            + oLevel.getName() + " and later (--min-api " + oLevel.getLevel() + ")");
       }
       DexApplication application =
           new ApplicationReader(inputApp, options, timing).read(executorService);
@@ -392,8 +393,6 @@
 
       options.printWarnings();
       return new CompilationResult(androidApp, application, appInfo);
-    } catch (MainDexError mainDexError) {
-      throw new CompilationError(mainDexError.getMessageForR8());
     } catch (ExecutionException e) {
       unwrapExecutionException(e);
       throw new AssertionError(e); // unwrapping method should have thrown
@@ -445,7 +444,7 @@
    * @return the compilation result.
    */
   public static AndroidApp run(R8Command command)
-      throws IOException, CompilationException, ProguardRuleParserException {
+      throws IOException, CompilationException {
     ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
     try {
       return run(command, executorService);
@@ -516,7 +515,7 @@
    * @return the compilation result.
    */
   public static AndroidApp run(R8Command command, ExecutorService executor)
-      throws IOException, CompilationException, ProguardRuleParserException {
+      throws IOException, CompilationException {
     InternalOptions options = command.getInternalOptions();
     AndroidApp outputApp =
         runForTesting(command.getInputApp(), options, executor).androidApp;
@@ -562,8 +561,7 @@
       cause.printStackTrace();
       System.exit(1);
     } catch (CompilationException e) {
-      System.err.println("Compilation failed: " + e.getMessage());
-      System.err.println(USAGE_MESSAGE);
+      System.err.println("Compilation failed: " + e.getMessageForR8());
       System.exit(1);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 775c1b3..890d570 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -7,6 +7,9 @@
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
@@ -17,19 +20,18 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Consumer;
 
-public class R8Command extends BaseCommand {
+public class R8Command extends BaseCompilerCommand {
 
-  public static class Builder extends BaseCommand.Builder<R8Command, Builder> {
+  public static class Builder extends BaseCompilerCommand.Builder<R8Command, Builder> {
 
-    private final List<Path> mainDexRules = new ArrayList<>();
+    private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
     private Path mainDexListOutput = null;
     private Consumer<ProguardConfiguration.Builder> proguardConfigurationConsumer = null;
-    private final List<Path> proguardConfigFiles = new ArrayList<>();
+    private final List<ProguardConfigurationSource> proguardConfigs = new ArrayList<>();
     private Optional<Boolean> treeShaking = Optional.empty();
     private Optional<Boolean> discardedChecker = Optional.empty();
     private Optional<Boolean> minification = Optional.empty();
@@ -76,16 +78,28 @@
     /**
      * Add proguard configuration file resources for automatic main dex list calculation.
      */
-    public Builder addMainDexRules(Path... paths) {
-      Collections.addAll(mainDexRules, paths);
+    public Builder addMainDexRulesFiles(Path... paths) {
+      for (Path path : paths) {
+        mainDexRules.add(new ProguardConfigurationSourceFile(path));
+      }
       return self();
     }
 
     /**
      * Add proguard configuration file resources for automatic main dex list calculation.
      */
-    public Builder addMainDexRules(List<Path> paths) {
-      mainDexRules.addAll(paths);
+    public Builder addMainDexRulesFiles(List<Path> paths) {
+      for (Path path : paths) {
+        mainDexRules.add(new ProguardConfigurationSourceFile(path));
+      }
+      return self();
+    }
+
+    /**
+     * Add proguard configuration for automatic main dex list calculation.
+     */
+    public Builder addMainDexRules(List<String> lines) {
+      mainDexRules.add(new ProguardConfigurationSourceStrings(lines));
       return self();
     }
 
@@ -98,7 +112,9 @@
      * Add proguard configuration file resources.
      */
     public Builder addProguardConfigurationFiles(Path... paths) {
-      Collections.addAll(proguardConfigFiles, paths);
+      for (Path path : paths) {
+        proguardConfigs.add(new ProguardConfigurationSourceFile(path));
+      }
       return self();
     }
 
@@ -106,7 +122,17 @@
      * Add proguard configuration file resources.
      */
     public Builder addProguardConfigurationFiles(List<Path> paths) {
-      proguardConfigFiles.addAll(paths);
+      for (Path path : paths) {
+        proguardConfigs.add(new ProguardConfigurationSourceFile(path));
+      }
+      return self();
+    }
+
+    /**
+     * Add proguard configuration.
+     */
+    public Builder addProguardConfiguration(List<String> lines) {
+      proguardConfigs.add(new ProguardConfigurationSourceStrings(lines));
       return self();
     }
 
@@ -184,12 +210,12 @@
         mainDexKeepRules = parser.getConfig().getRules();
       }
       ProguardConfiguration configuration;
-      if (proguardConfigFiles.isEmpty()) {
+      if (proguardConfigs.isEmpty()) {
         configuration = ProguardConfiguration.defaultConfiguration(factory);
       } else {
         ProguardConfigurationParser parser = new ProguardConfigurationParser(factory);
         try {
-          parser.parse(proguardConfigFiles);
+          parser.parse(proguardConfigs);
         } catch (ProguardRuleParserException e) {
           throw new CompilationException(e.getMessage(), e.getCause());
         }
@@ -324,7 +350,7 @@
       } else if (arg.equals("--no-minification")) {
         builder.setMinification(false);
       } else if (arg.equals("--main-dex-rules")) {
-        builder.addMainDexRules(Paths.get(args[++i]));
+        builder.addMainDexRulesFiles(Paths.get(args[++i]));
       } else if (arg.equals("--main-dex-list")) {
         builder.addMainDexListFiles(Paths.get(args[++i]));
       } else if (arg.equals("--main-dex-list-output")) {
diff --git a/src/main/java/com/android/tools/r8/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index ee4477d..81d5f9a 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.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.errors.DexOverflowException;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -174,7 +175,7 @@
   }
 
   private static void writeApp(DexApplication app, Path output, ExecutorService executor)
-      throws IOException, ExecutionException {
+      throws IOException, ExecutionException, DexOverflowException {
     InternalOptions options = new InternalOptions();
     AppInfo appInfo = new AppInfo(app);
     ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null, null, null);
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectOptions.java b/src/main/java/com/android/tools/r8/bisect/BisectOptions.java
index d0051dc..8bd2c90 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectOptions.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectOptions.java
@@ -121,8 +121,7 @@
     return new BisectOptions(goodBuild, badBuild, stateFile, command, output, result);
   }
 
-  private static <T> T require(OptionSet options, OptionSpec<T> option, String flag)
-      throws IOException {
+  private static <T> T require(OptionSet options, OptionSpec<T> option, String flag) {
     T value = options.valueOf(option);
     if (value != null) {
       return value;
@@ -130,7 +129,7 @@
     throw new CompilationError("Missing required option: --" + flag);
   }
 
-  private static File exists(String path, String flag) throws IOException {
+  private static File exists(String path, String flag) {
     File file = new File(path);
     if (file.exists()) {
       return file;
@@ -138,7 +137,7 @@
     throw new CompilationError("File --" + flag + ": " + file + " does not exist");
   }
 
-  private static File directoryExists(String path, String flag) throws IOException {
+  private static File directoryExists(String path, String flag) {
     File file = new File(path);
     if (file.exists() && file.isDirectory()) {
       return file;
diff --git a/src/main/java/com/android/tools/r8/code/Instruction.java b/src/main/java/com/android/tools/r8/code/Instruction.java
index 85e0318..504b605 100644
--- a/src/main/java/com/android/tools/r8/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/code/Instruction.java
@@ -228,6 +228,7 @@
     throw new InternalCompilerError("Instruction " + payloadUser + " is not a payload user");
   }
 
+  @Override
   public String toString() {
     return toString(null);
   }
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 9bcf510..fdd5964 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -36,11 +36,11 @@
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.zip.ZipEntry;
-import java.util.zip.ZipException;
-import java.util.zip.ZipInputStream;
+import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 import joptsimple.OptionParser;
 import joptsimple.OptionSet;
@@ -250,7 +250,7 @@
       }
     }
 
-    private DxCompatOptions(OptionSet options, Spec spec) throws DxParseError {
+    private DxCompatOptions(OptionSet options, Spec spec) {
       help = options.has(spec.help);
       debug = options.has(spec.debug);
       verbose = options.has(spec.verbose);
@@ -383,7 +383,7 @@
     }
 
     if (dexArgs.dumpTo != null) {
-      throw new Unimplemented("dump-to file not yet supported");
+      System.out.println("dump-to file not yet supported");
     }
 
     if (dexArgs.positions == PositionInfo.NONE) {
@@ -529,16 +529,16 @@
         // For each input archive file, add all class files within.
         for (Path input : inputs) {
           if (isArchive(input)) {
-            try (ZipInputStream in = new ZipInputStream(Files.newInputStream(input))) {
-              ZipEntry entry;
-              while ((entry = in.getNextEntry()) != null) {
+            try (ZipFile zipFile = new ZipFile(input.toFile())) {
+              final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+              while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
                 if (isClassFile(Paths.get(entry.getName()))) {
-                  addEntry(entry.getName(), in, out);
+                  try (InputStream entryStream = zipFile.getInputStream(entry)) {
+                    addEntry(entry.getName(), entryStream, out);
+                  }
                 }
               }
-            } catch (ZipException e) {
-              throw new CompilationError(
-                  "Zip error while reading '" + input + "': " + e.getMessage(), e);
             }
           }
         }
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 cca3520..a07a4a3 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -35,7 +35,6 @@
 import com.android.tools.r8.utils.MainDexList;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
-import com.google.common.io.Closer;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -130,8 +129,7 @@
   }
 
   private void readProguardMap(DexApplication.Builder builder, ExecutorService executorService,
-      List<Future<?>> futures)
-      throws IOException {
+      List<Future<?>> futures) {
     // Read the Proguard mapping file in parallel with DexCode and DexProgramClass items.
     if (inputApp.hasProguardMap()) {
       futures.add(executorService.submit(() -> {
@@ -145,8 +143,7 @@
   }
 
   private void readMainDexList(DexApplication.Builder builder, ExecutorService executorService,
-      List<Future<?>> futures)
-      throws IOException {
+      List<Future<?>> futures) {
     if (inputApp.hasMainDexList()) {
       futures.add(executorService.submit(() -> {
         for (Resource resource : inputApp.getMainDexListResources()) {
@@ -184,7 +181,7 @@
     }
 
     private <T extends DexClass> void readDexSources(List<Resource> dexSources,
-        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+        ClassKind classKind, Queue<T> classes) throws IOException {
       if (dexSources.size() > 0) {
         List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
         int computedMinApiLevel = options.minApiLevel;
@@ -211,7 +208,7 @@
     }
 
     private <T extends DexClass> void readClassSources(List<Resource> classSources,
-        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+        ClassKind classKind, Queue<T> classes) {
       JarClassFileReader reader = new JarClassFileReader(
           application, classKind.bridgeConsumer(classes::add));
       // Read classes in parallel.
@@ -227,7 +224,7 @@
       }
     }
 
-    void readSources() throws IOException, ExecutionException {
+    void readSources() throws IOException {
       readDexSources(inputApp.getDexProgramResources(), PROGRAM, programClasses);
       readClassSources(inputApp.getClassProgramResources(), PROGRAM, programClasses);
     }
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 412b291..46ee85c 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -4,10 +4,8 @@
 package com.android.tools.r8.dex;
 
 import com.android.tools.r8.ApiLevelException;
-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.errors.DexOverflowException;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
@@ -129,7 +127,7 @@
   }
 
   public AndroidApp write(PackageDistribution packageDistribution, ExecutorService executorService)
-      throws IOException, ExecutionException {
+      throws IOException, ExecutionException, DexOverflowException {
     application.timing.begin("DexApplication.write");
     try {
       application.dexItemFactory.sort(namingLens);
@@ -243,7 +241,7 @@
         .replace('.', '/') + ".class";
   }
 
-  private byte[] writeMainDexList() throws IOException {
+  private byte[] writeMainDexList() {
     if (application.mainDexList.isEmpty()) {
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/dex/DexFileReader.java b/src/main/java/com/android/tools/r8/dex/DexFileReader.java
index 09b3c33..7bf5e8c 100644
--- a/src/main/java/com/android/tools/r8/dex/DexFileReader.java
+++ b/src/main/java/com/android/tools/r8/dex/DexFileReader.java
@@ -81,7 +81,7 @@
     return parseMapFrom(new DexFile(stream));
   }
 
-  private static Segment[] parseMapFrom(DexFile dex) throws IOException {
+  private static Segment[] parseMapFrom(DexFile dex) {
     DexFileReader reader = new DexFileReader(dex, ClassKind.PROGRAM, new DexItemFactory());
     return reader.segments;
   }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 38e292c..d820747 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -47,6 +47,7 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LebUtils;
 import com.android.tools.r8.utils.ThrowingConsumer;
@@ -296,8 +297,7 @@
     if (method.accessFlags.isStatic()) {
       if (!options.canUseDefaultAndStaticInterfaceMethods()) {
         throw new ApiLevelException(
-            Constants.ANDROID_N_API,
-            "Android N",
+            AndroidApiLevel.N,
             "Static interface methods",
             method.method.toSourceString());
       }
@@ -310,8 +310,7 @@
       if (!method.accessFlags.isAbstract() && !method.accessFlags.isPrivate() &&
           !options.canUseDefaultAndStaticInterfaceMethods()) {
         throw new ApiLevelException(
-            Constants.ANDROID_N_API,
-            "Android N",
+            AndroidApiLevel.N,
             "Default interface methods",
             method.method.toSourceString());
       }
@@ -322,8 +321,7 @@
         return;
       }
       throw new ApiLevelException(
-          Constants.ANDROID_N_API,
-          "Android N",
+          AndroidApiLevel.N,
           "Private interface methods",
           method.method.toSourceString());
     }
@@ -445,10 +443,10 @@
 
   private void writeFieldItem(DexField field) {
     int classIdx = mapping.getOffsetFor(field.clazz);
-    assert (short) classIdx == classIdx;
+    assert (classIdx & 0xFFFF) == classIdx;
     dest.putShort((short) classIdx);
     int typeIdx = mapping.getOffsetFor(field.type);
-    assert (short) typeIdx == typeIdx;
+    assert (typeIdx & 0xFFFF) == typeIdx;
     dest.putShort((short) typeIdx);
     DexString name = namingLens.lookupName(field);
     dest.putInt(mapping.getOffsetFor(name));
@@ -456,10 +454,10 @@
 
   private void writeMethodItem(DexMethod method) {
     int classIdx = mapping.getOffsetFor(method.holder);
-    assert (short) classIdx == classIdx;
+    assert (classIdx & 0xFFFF) == classIdx;
     dest.putShort((short) classIdx);
     int protoIdx = mapping.getOffsetFor(method.proto);
-    assert (short) protoIdx == protoIdx;
+    assert (protoIdx & 0xFFFF) == protoIdx;
     dest.putShort((short) protoIdx);
     DexString name = namingLens.lookupName(method);
     dest.putInt(mapping.getOffsetFor(name));
@@ -694,7 +692,7 @@
       assert methodHandle.isFieldHandle();
       fieldOrMethodIdx = mapping.getOffsetFor(methodHandle.asField());
     }
-    assert (short) fieldOrMethodIdx == fieldOrMethodIdx;
+    assert (fieldOrMethodIdx & 0xFFFF) == fieldOrMethodIdx;
     dest.putShort((short) fieldOrMethodIdx);
     dest.putShort((short) 0); // unused
   }
@@ -1352,8 +1350,7 @@
   private void checkThatInvokeCustomIsAllowed() throws ApiLevelException {
     if (!options.canUseInvokeCustom()) {
       throw new ApiLevelException(
-          Constants.ANDROID_O_API,
-          "Android O",
+          AndroidApiLevel.O,
           "Invoke-customs",
           null /* sourceString */);
     }
diff --git a/src/main/java/com/android/tools/r8/dex/Segment.java b/src/main/java/com/android/tools/r8/dex/Segment.java
index 0d782d6..7836438 100644
--- a/src/main/java/com/android/tools/r8/dex/Segment.java
+++ b/src/main/java/com/android/tools/r8/dex/Segment.java
@@ -71,6 +71,7 @@
     }
   }
 
+  @Override
   public String toString() {
     return typeName() + " @" + offset + " " + length;
   }
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 cbca177..c40d5a5 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -5,7 +5,7 @@
 
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
-import com.android.tools.r8.errors.MainDexError;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
@@ -173,11 +173,11 @@
     return isFull(transaction.getNumberOfMethods(), transaction.getNumberOfFields(), MAX_ENTRIES);
   }
 
-  void throwIfFull(boolean hasMainDexList) {
+  void throwIfFull(boolean hasMainDexList) throws DexOverflowException {
     if (!isFull()) {
       return;
     }
-    throw new MainDexError(
+    throw new DexOverflowException(
         hasMainDexList,
         transaction.getNumberOfMethods(),
         transaction.getNumberOfFields(),
@@ -217,7 +217,8 @@
       this.writer = writer;
     }
 
-    public abstract Map<Integer, VirtualFile> run() throws ExecutionException, IOException;
+    public abstract Map<Integer, VirtualFile> run()
+        throws ExecutionException, IOException, DexOverflowException;
   }
 
   public static class FilePerClassDistributor extends Distributor {
@@ -226,7 +227,8 @@
       super(writer);
     }
 
-    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+    @Override
+    public Map<Integer, VirtualFile> run() {
       // Assign dedicated virtual files for all program classes.
       for (DexProgramClass clazz : application.classes()) {
         VirtualFile file = new VirtualFile(nameToFileMap.size(), writer.namingLens);
@@ -258,7 +260,7 @@
       originalNames = computeOriginalNameMapping(classes, application.getProguardMap());
     }
 
-    protected void fillForMainDexList(Set<DexProgramClass> classes) {
+    protected void fillForMainDexList(Set<DexProgramClass> classes) throws DexOverflowException {
       if (!application.mainDexList.isEmpty()) {
         VirtualFile mainDexFile = nameToFileMap.get(0);
         for (DexType type : application.mainDexList) {
@@ -322,7 +324,8 @@
       this.fillStrategy = FillStrategy.FILL_MAX;
     }
 
-    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+    @Override
+    public Map<Integer, VirtualFile> run() throws IOException, DexOverflowException {
       // First fill required classes into the main dex file.
       fillForMainDexList(classes);
       if (classes.isEmpty()) {
@@ -357,7 +360,8 @@
     }
 
     @Override
-    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+    public Map<Integer, VirtualFile> run()
+        throws ExecutionException, IOException, DexOverflowException {
       // Add all classes to the main dex file.
       for (DexProgramClass programClass : classes) {
         mainDexFile.addClass(programClass);
@@ -381,7 +385,9 @@
       this.executorService = executorService;
     }
 
-    public Map<Integer, VirtualFile> run() throws ExecutionException, IOException {
+    @Override
+    public Map<Integer, VirtualFile> run()
+        throws ExecutionException, IOException, DexOverflowException {
       // 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.
diff --git a/src/main/java/com/android/tools/r8/errors/DexOverflowException.java b/src/main/java/com/android/tools/r8/errors/DexOverflowException.java
new file mode 100644
index 0000000..a94d206
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/DexOverflowException.java
@@ -0,0 +1,91 @@
+// 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.errors;
+
+import com.android.tools.r8.CompilationException;
+
+/**
+ * Signals when there were too many items to fit in a given dex file.
+ */
+public class DexOverflowException extends CompilationException {
+
+  private final boolean hasMainDexList;
+  private final long numOfMethods;
+  private final long numOfFields;
+  private final long maxNumOfEntries;
+
+  public DexOverflowException(
+      boolean hasMainDexList, long numOfMethods, long numOfFields, long maxNumOfEntries) {
+    super();
+    this.hasMainDexList = hasMainDexList;
+    this.numOfMethods = numOfMethods;
+    this.numOfFields = numOfFields;
+    this.maxNumOfEntries = maxNumOfEntries;
+  }
+
+  private StringBuilder getGeneralMessage() {
+    StringBuilder messageBuilder = new StringBuilder();
+    // General message: Cannot fit.
+    messageBuilder.append("Cannot fit requested classes in ");
+    messageBuilder.append(hasMainDexList ? "the main-" : "a single ");
+    messageBuilder.append("dex file");
+
+    return messageBuilder;
+  }
+
+  private String getNumberRelatedMessage() {
+    StringBuilder messageBuilder = new StringBuilder();
+    // Show the numbers of methods and/or fields that exceed the limit.
+    if (numOfMethods > maxNumOfEntries) {
+      messageBuilder.append("# methods: ");
+      messageBuilder.append(numOfMethods);
+      messageBuilder.append(" > ").append(maxNumOfEntries);
+      if (numOfFields > maxNumOfEntries) {
+        messageBuilder.append(" ; ");
+      }
+    }
+    if (numOfFields > maxNumOfEntries) {
+      messageBuilder.append("# fields: ");
+      messageBuilder.append(numOfFields);
+      messageBuilder.append(" > ").append(maxNumOfEntries);
+    }
+
+    return messageBuilder.toString();
+  }
+
+  @Override
+  public String getMessage() {
+    // Default message
+    return getGeneralMessage()
+        .append(" (")
+        .append(getNumberRelatedMessage())
+        .append(")")
+        .toString();
+  }
+
+  @Override
+  public String getMessageForD8() {
+    StringBuilder messageBuilder = getGeneralMessage();
+    if (!hasMainDexList) {
+      messageBuilder.append(". ");
+      messageBuilder.append("Try supplying a main-dex list");
+    }
+    messageBuilder.append(".").append(System.getProperty("line.separator"));
+    messageBuilder.append(getNumberRelatedMessage());
+    return messageBuilder.toString();
+  }
+
+  @Override
+  public String getMessageForR8() {
+    StringBuilder messageBuilder = getGeneralMessage();
+    if (!hasMainDexList) {
+      messageBuilder.append(". ");
+      messageBuilder.append("Try supplying a main-dex list or main dex rules");
+    }
+    messageBuilder.append(".").append(System.getProperty("line.separator"));
+    messageBuilder.append(getNumberRelatedMessage());
+    return messageBuilder.toString();
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/errors/MainDexError.java b/src/main/java/com/android/tools/r8/errors/MainDexError.java
deleted file mode 100644
index 4c23aca..0000000
--- a/src/main/java/com/android/tools/r8/errors/MainDexError.java
+++ /dev/null
@@ -1,89 +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.errors;
-
-/**
- * Exception regarding main-dex list and main dex rules.
- *
- * Depending on tool kind, this exception should be massaged, e.g., adding appropriate suggestions,
- * and re-thrown as {@link CompilationError}, which will be in turn informed to the user as an
- * expected compilation error.
- */
-public class MainDexError extends RuntimeException {
-
-  private final boolean hasMainDexList;
-  private final long numOfMethods;
-  private final long numOfFields;
-  private final long maxNumOfEntries;
-
-  public MainDexError(
-      boolean hasMainDexList, long numOfMethods, long numOfFields, long maxNumOfEntries) {
-    this.hasMainDexList = hasMainDexList;
-    this.numOfMethods = numOfMethods;
-    this.numOfFields = numOfFields;
-    this.maxNumOfEntries = maxNumOfEntries;
-  }
-
-  private String getGeneralMessage() {
-    StringBuilder messageBuilder = new StringBuilder();
-    // General message: Cannot fit.
-    messageBuilder.append("Cannot fit requested classes in ");
-    messageBuilder.append(hasMainDexList ? "the main-" : "a single ");
-    messageBuilder.append("dex file.\n");
-
-    return messageBuilder.toString();
-  }
-
-  private String getNumberRelatedMessage() {
-    StringBuilder messageBuilder = new StringBuilder();
-    // Show the numbers of methods and/or fields that exceed the limit.
-    if (numOfMethods > maxNumOfEntries) {
-      messageBuilder.append("# methods: ");
-      messageBuilder.append(numOfMethods);
-      messageBuilder.append(" > ").append(maxNumOfEntries).append('\n');
-    }
-    if (numOfFields > maxNumOfEntries) {
-      messageBuilder.append("# fields: ");
-      messageBuilder.append(numOfFields);
-      messageBuilder.append(" > ").append(maxNumOfEntries).append('\n');
-    }
-
-    return messageBuilder.toString();
-  }
-
-  @Override
-  public String getMessage() {
-    // Placeholder to generate a general error message for other (minor) utilities:
-    //   Bisect, disassembler, dexsegments.
-    // Implement tool-specific error message generator, like D8 and R8 below, if necessary.
-    return getGeneralMessage() + getNumberRelatedMessage();
-  }
-
-  public String getMessageForD8() {
-    StringBuilder messageBuilder = new StringBuilder();
-    messageBuilder.append(getGeneralMessage());
-    if (hasMainDexList) {
-      messageBuilder.append("Classes required by the main-dex list ");
-      messageBuilder.append("do not fit in one dex.\n");
-    } else {
-      messageBuilder.append("Try supplying a main-dex list.\n");
-    }
-    messageBuilder.append(getNumberRelatedMessage());
-    return messageBuilder.toString();
-  }
-
-  public String getMessageForR8() {
-    StringBuilder messageBuilder = new StringBuilder();
-    messageBuilder.append(getGeneralMessage());
-    if (hasMainDexList) {
-      messageBuilder.append("Classes required by main dex rules and the main-dex list ");
-      messageBuilder.append("do not fit in one dex.\n");
-    } else {
-      messageBuilder.append("Try supplying a main-dex list or main dex rules.\n");
-    }
-    messageBuilder.append(getNumberRelatedMessage());
-    return messageBuilder.toString();
-  }
-
-}
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 82febb9..50c22b8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
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 1a8602c..8da2178 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -82,6 +82,17 @@
     this.debugInfo = debugInfo;
   }
 
+  public boolean hasDebugPositions() {
+    if (debugInfo != null) {
+      for (DexDebugEvent event : debugInfo.events) {
+        if (event instanceof DexDebugEvent.Default) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
     if (debugInfo == null) {
       return null;
@@ -143,7 +154,7 @@
     return false;
   }
 
-  boolean isEmptyVoidMethod() {
+  public boolean isEmptyVoidMethod() {
     return instructions.length == 1 && instructions[0] instanceof ReturnVoid;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index ffaf42b..3d68ace 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -91,8 +91,8 @@
       // If this is the end of the block clear out the pending state.
       pendingLocals = null;
       pendingLocalChanges = false;
-    } else {
-      // For non-exit / pc-advancing instructions emit any pending changes.
+    } else if (pc != emittedPc) {
+      // For non-exit / pc-advancing instructions emit any pending changes once possible.
       emitLocalChanges(pc);
     }
   }
@@ -224,6 +224,7 @@
       DexString nextFile,
       List<DexDebugEvent> events,
       DexItemFactory factory) {
+    assert previousPc != nextPc;
     int pcDelta = previousPc == NO_PC_INFO ? nextPc : nextPc - previousPc;
     int lineDelta = nextLine == NO_LINE_INFO ? 0 : nextLine - previousLine;
     assert pcDelta >= 0;
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 8d28546..b76177b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
@@ -107,13 +108,13 @@
     return accessFlags.isConstructor() && accessFlags.isStatic();
   }
 
-  public boolean isInliningCandidate(DexEncodedMethod container, boolean alwaysInline,
+  public boolean isInliningCandidate(DexEncodedMethod container, Reason inliningReason,
       AppInfoWithSubtyping appInfo) {
     if (isClassInitializer()) {
       // This will probably never happen but never inline a class initializer.
       return false;
     }
-    if (alwaysInline) {
+    if (inliningReason == Reason.FORCE) {
       return true;
     }
     switch (compilationState) {
@@ -208,6 +209,11 @@
     code = null;
   }
 
+  public boolean hasDebugPositions() {
+    assert code != null && code.isDexCode();
+    return code.asDexCode().hasDebugPositions();
+  }
+
   public String qualifiedName() {
     return method.qualifiedName();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 7dc77de..527f27b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -162,7 +162,7 @@
       "Ldalvik/annotation/SourceDebugExtension;");
   public final DexType annotationThrows = createType("Ldalvik/annotation/Throws;");
 
-  public void clearSubtypeInformation() {
+  public synchronized void clearSubtypeInformation() {
     types.values().forEach(DexType::clearSubtypeInformation);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index 08c8a1c..73b3242 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -151,6 +151,10 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueByte(value);
     }
 
+    public byte getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeHeader(VALUE_BYTE, 0, dest);
@@ -196,6 +200,10 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueShort(value);
     }
 
+    public short getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeIntegerTo(VALUE_SHORT, value, Short.BYTES, dest);
@@ -240,6 +248,10 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueChar(value);
     }
 
+    public char getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       dest.forward(1);
@@ -288,6 +300,10 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueInt(value);
     }
 
+    public int getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeIntegerTo(VALUE_INT, value, Integer.BYTES, dest);
@@ -332,6 +348,10 @@
       return value == DEFAULT.value ? DEFAULT : new DexValueLong(value);
     }
 
+    public long getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeIntegerTo(VALUE_LONG, value, Long.BYTES, dest);
@@ -376,6 +396,10 @@
       return Float.compare(value, DEFAULT.value) == 0 ? DEFAULT : new DexValueFloat(value);
     }
 
+    public float getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       dest.forward(1);
@@ -420,6 +444,10 @@
       return Double.compare(value, DEFAULT.value) == 0 ? DEFAULT : new DexValueDouble(value);
     }
 
+    public double getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       dest.forward(1);
@@ -508,6 +536,10 @@
       super(value);
     }
 
+    public DexString getValue() {
+      return value;
+    }
+
     @Override
     protected byte getValueKind() {
       return VALUE_STRING;
@@ -710,6 +742,10 @@
     private DexValueNull() {
     }
 
+    public Object getValue() {
+      return null;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeHeader(VALUE_NULL, 0, dest);
@@ -751,6 +787,10 @@
       return value ? TRUE : FALSE;
     }
 
+    public boolean getValue() {
+      return value;
+    }
+
     @Override
     public void writeTo(DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
       writeHeader(VALUE_BOOLEAN, value ? 1 : 0, dest);
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 b9173dd..3abe7c5 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -118,18 +118,21 @@
     @Override
     public DexType lookupType(DexType type, DexEncodedMethod context) {
       if (type.isArrayType()) {
-        DexType result = arrayTypeCache.get(type);
-        if (result == null) {
-          DexType baseType = type.toBaseType(dexItemFactory);
-          DexType newType = lookupType(baseType, context);
-          if (baseType == newType) {
-            result = type;
-          } else {
-            result = type.replaceBaseType(newType, dexItemFactory);
+        synchronized(this) {
+          // This block need to be synchronized due to arrayTypeCache.
+          DexType result = arrayTypeCache.get(type);
+          if (result == null) {
+            DexType baseType = type.toBaseType(dexItemFactory);
+            DexType newType = lookupType(baseType, context);
+            if (baseType == newType) {
+              result = type;
+            } else {
+              result = type.replaceBaseType(newType, dexItemFactory);
+            }
+            arrayTypeCache.put(type, result);
           }
-          arrayTypeCache.put(type, result);
+          return result;
         }
-        return result;
       }
       DexType previous = previousLense.lookupType(type, context);
       return typeMap.getOrDefault(previous, previous);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
index f5bc204..40a4776 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
@@ -140,29 +140,13 @@
       assert newInstruction.outValue() != null;
       current.outValue().replaceUsers(newInstruction.outValue());
     }
-    for (Value value : current.getDebugValues()) {
-      replaceInstructionInList(current, newInstruction, value.getDebugLocalStarts());
-      replaceInstructionInList(current, newInstruction, value.getDebugLocalEnds());
-      value.removeDebugUser(current);
-      newInstruction.addDebugValue(value);
-    }
+    current.moveDebugValues(newInstruction);
     newInstruction.setBlock(block);
     listIterator.remove();
     listIterator.add(newInstruction);
     current.clearBlock();
   }
 
-  private static void replaceInstructionInList(
-      Instruction instruction,
-      Instruction newInstruction,
-      List<Instruction> instructions) {
-    for (int i = 0; i < instructions.size(); i++) {
-      if (instructions.get(i) == instruction) {
-        instructions.set(i, newInstruction);
-      }
-    }
-  }
-
   public BasicBlock split(IRCode code, ListIterator<BasicBlock> blocksIterator) {
     List<BasicBlock> blocks = code.blocks;
     assert blocksIterator == null || IteratorUtils.peekPrevious(blocksIterator) == block;
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 e376361..ef5a025 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
@@ -26,7 +26,6 @@
     // are all for fixed register values. All other values that are used as the destination for
     // const number instructions should be marked as constants.
     assert dest.isFixedRegisterValue() || dest.definition.isConstNumber();
-    assert type != ConstType.OBJECT;
     this.type = type;
     this.value = value;
   }
@@ -50,7 +49,7 @@
   }
 
   public int getIntValue() {
-    assert type == ConstType.INT || type == ConstType.INT_OR_FLOAT;
+    assert type == ConstType.INT || type == ConstType.INT_OR_FLOAT || type == ConstType.OBJECT;
     return (int) value;
   }
 
@@ -101,7 +100,7 @@
     }
 
     int register = builder.allocatedRegister(dest(), getNumber());
-    if (MoveType.fromConstType(type) == MoveType.SINGLE) {
+    if (MoveType.fromConstType(type) == MoveType.SINGLE || type == ConstType.OBJECT) {
       assert NumberUtils.is32Bit(value);
       if ((register & 0xf) == register && NumberUtils.is4Bit(value)) {
         builder.add(this, new Const4(register, (int) value));
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 8fd9bf0..7dc5e75 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
@@ -88,7 +88,7 @@
 
   @Override
   public boolean canBeFolded() {
-    return super.canBeFolded() && !rightValue().getConstInstruction().asConstNumber().isZero();
+    return super.canBeFolded() && !rightValue().isZero();
   }
 
   @Override
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 e99dbf1..1cc21a3 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
@@ -74,8 +74,9 @@
     if (debugValues == null) {
       debugValues = new HashSet<>();
     }
-    debugValues.add(value);
-    value.addDebugUser(this);
+    if (debugValues.add(value)) {
+      value.addDebugUser(this);
+    }
   }
 
   public static void clearUserInfo(Instruction instruction) {
@@ -94,19 +95,17 @@
 
   public abstract void buildDex(DexBuilder builder);
 
-  public void replaceValue(Value oldValue, Value newValue, List<Instruction> toRemove) {
+  public void replaceValue(Value oldValue, Value newValue) {
     for (int i = 0; i < inValues.size(); i++) {
       if (oldValue == inValues.get(i)) {
         inValues.set(i, newValue);
         newValue.addUser(this);
-        toRemove.add(this);
       }
     }
   }
 
-  public void replaceDebugValue(Value oldValue, Value newValue, List<Instruction> toRemove) {
+  public void replaceDebugValue(Value oldValue, Value newValue) {
     if (debugValues.remove(oldValue)) {
-      toRemove.add(this);
       if (newValue.getLocalInfo() != null) {
         // TODO(zerny): Insert a write if replacing a phi with different debug-local info.
         addDebugValue(newValue);
@@ -115,6 +114,22 @@
     }
   }
 
+  public void moveDebugValues(Instruction target) {
+    if (debugValues == null) {
+      return;
+    }
+    for (Value value : debugValues) {
+      value.replaceDebugUser(this, target);
+    }
+    debugValues.clear();
+  }
+
+  public void moveDebugValue(Value value, Instruction target) {
+    assert debugValues.contains(value);
+    value.replaceDebugUser(this, target);
+    debugValues.remove(value);
+  }
+
   /**
    * Returns the basic block containing this instruction.
    */
diff --git a/src/main/java/com/android/tools/r8/ir/code/MoveException.java b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
index 3dff4f3..61ed928 100644
--- a/src/main/java/com/android/tools/r8/ir/code/MoveException.java
+++ b/src/main/java/com/android/tools/r8/ir/code/MoveException.java
@@ -27,6 +27,7 @@
   }
 
   public void setPosition(DebugPosition position) {
+    assert this.position == null;
     this.position = position;
   }
 
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 96069bf..65995ff 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
@@ -155,22 +155,20 @@
     current.removePhiUser(this);
   }
 
-  void replaceTrivialPhi(Value current, Value newValue, List<Phi> toRemove) {
+  void replaceOperand(Value current, Value newValue) {
     for (int i = 0; i < operands.size(); i++) {
       if (operands.get(i) == current) {
         operands.set(i, newValue);
         newValue.addPhiUser(this);
-        toRemove.add(this);
       }
     }
   }
 
-  void replaceTrivialDebugPhi(Value current, Value newValue, List<Phi> toRemove) {
+  void replaceDebugValue(Value current, Value newValue) {
     assert current.getLocalInfo() != null;
     assert current.getLocalInfo() == newValue.getLocalInfo();
     if (debugValues.remove(current)) {
       addDebugValue(newValue);
-      toRemove.add(this);
     }
   }
 
@@ -228,7 +226,7 @@
     {
       Set<Phi> phiUsersToSimplify = uniquePhiUsers();
       // Replace this phi with the unique value in all users.
-      replaceInUsers(same);
+      replaceUsers(same);
       // Try to simplify phi users that might now have become trivial.
       for (Phi user : phiUsersToSimplify) {
         user.removeTrivialPhi();
@@ -275,9 +273,9 @@
   }
 
   private boolean isSingleConstZero(Value value) {
-    return value.definition != null && value.definition.isConstNumber() &&
-        value.definition.asConstNumber().isZero() &&
-        value.outType() == MoveType.SINGLE;
+    return value.definition != null
+        && value.outType() == MoveType.SINGLE
+        && value.isZero();
   }
 
   private MoveType computeOutType(Set<Phi> active) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Rem.java b/src/main/java/com/android/tools/r8/ir/code/Rem.java
index 6521ae7..a257109 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Rem.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Rem.java
@@ -88,7 +88,7 @@
 
   @Override
   public boolean canBeFolded() {
-    return super.canBeFolded() && !rightValue().getConstInstruction().asConstNumber().isZero();
+    return super.canBeFolded() && !rightValue().isZero();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Switch.java b/src/main/java/com/android/tools/r8/ir/code/Switch.java
index 6def63a..08ab002 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Switch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Switch.java
@@ -13,6 +13,8 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.google.common.primitives.Ints;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.util.List;
 
 public class Switch extends JumpInstruction {
@@ -159,6 +161,11 @@
     }
   }
 
+  // Estimated size of the resulting dex instruction in code units (excluding the payload).
+  public static int estimatedDexSize() {
+    return 3;
+  }
+
   public int numberOfKeys() {
     return keys.length;
   }
@@ -175,6 +182,14 @@
     return targetBlockIndices;
   }
 
+  public Int2ReferenceSortedMap<BasicBlock> getKeyToTargetMap() {
+    Int2ReferenceSortedMap<BasicBlock> result = new Int2ReferenceAVLTreeMap<>();
+    for (int i = 0; i < keys.length; i++) {
+      result.put(getKey(i), targetBlock(i));
+    }
+    return result;
+  }
+
   @Override
   public BasicBlock fallthroughBlock() {
     return getBlock().getSuccessors().get(fallthroughBlockIndex);
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index dfa8548..2880c87 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.code;
 
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.regalloc.LiveIntervals;
 import com.android.tools.r8.utils.InternalOptions;
@@ -11,9 +12,12 @@
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 public class Value {
@@ -21,17 +25,49 @@
   // Lazily allocated internal data for the debug information of locals.
   // This is wrapped in a class to avoid multiple pointers in the value structure.
   private static class DebugData {
+
     final DebugLocalInfo local;
-    Set<Instruction> users = new HashSet<>();
+    Map<Instruction, DebugUse> users = new HashMap<>();
     Set<Phi> phiUsers = new HashSet<>();
-    List<Instruction> localStarts = new ArrayList<>();
-    List<Instruction> localEnds = new ArrayList<>();
 
     DebugData(DebugLocalInfo local) {
       this.local = local;
     }
   }
 
+  // A debug-value user represents a point where the value is live, ends or starts.
+  // If a point is marked as both ending and starting then it is simply live, but we maintain
+  // the marker so as not to unintentionally end it if marked again.
+  private enum DebugUse {
+    LIVE, START, END, LIVE_FINAL;
+
+    DebugUse start() {
+      switch (this) {
+        case LIVE:
+        case START:
+          return START;
+        case END:
+        case LIVE_FINAL:
+          return LIVE_FINAL;
+        default:
+          throw new Unreachable();
+      }
+    }
+
+    DebugUse end() {
+      switch (this) {
+        case LIVE:
+        case END:
+          return END;
+        case START:
+        case LIVE_FINAL:
+          return LIVE_FINAL;
+        default:
+          throw new Unreachable();
+      }
+    }
+  }
+
   public static final Value UNDEFINED = new Value(-1, MoveType.OBJECT, null);
 
   protected final int number;
@@ -79,21 +115,49 @@
   }
 
   public List<Instruction> getDebugLocalStarts() {
-    return debugData.localStarts;
+    if (debugData == null) {
+      return Collections.emptyList();
+    }
+    List<Instruction> starts = new ArrayList<>(debugData.users.size());
+    for (Entry<Instruction, DebugUse> entry : debugData.users.entrySet()) {
+      if (entry.getValue() == DebugUse.START) {
+        starts.add(entry.getKey());
+      }
+    }
+    return starts;
   }
 
   public List<Instruction> getDebugLocalEnds() {
-    return debugData.localEnds;
+    if (debugData == null) {
+      return Collections.emptyList();
+    }
+    List<Instruction> ends = new ArrayList<>(debugData.users.size());
+    for (Entry<Instruction, DebugUse> entry : debugData.users.entrySet()) {
+      if (entry.getValue() == DebugUse.END) {
+        ends.add(entry.getKey());
+      }
+    }
+    return ends;
   }
 
   public void addDebugLocalStart(Instruction start) {
     assert start != null;
-    debugData.localStarts.add(start);
+    debugData.users.put(start, markStart(debugData.users.get(start)));
+  }
+
+  private DebugUse markStart(DebugUse use) {
+    assert use != null;
+    return use == null ? DebugUse.START : use.start();
   }
 
   public void addDebugLocalEnd(Instruction end) {
     assert end != null;
-    debugData.localEnds.add(end);
+    debugData.users.put(end, markEnd(debugData.users.get(end)));
+  }
+
+  private DebugUse markEnd(DebugUse use) {
+    assert use != null;
+    return use == null ? DebugUse.END : use.end();
   }
 
   public void linkTo(Value other) {
@@ -152,7 +216,7 @@
   }
 
   public Set<Instruction> debugUsers() {
-    return debugData == null ? null : Collections.unmodifiableSet(debugData.users);
+    return debugData == null ? null : Collections.unmodifiableSet(debugData.users.keySet());
   }
 
   public Set<Phi> debugPhiUsers() {
@@ -244,7 +308,7 @@
     if (isUninitializedLocal()) {
       return;
     }
-    debugData.users.add(user);
+    debugData.users.putIfAbsent(user, DebugUse.LIVE);
   }
 
   public void addDebugPhiUser(Phi user) {
@@ -263,11 +327,19 @@
   }
 
   public void removeDebugUser(Instruction user) {
-    debugData.users.remove(user);
+    if (debugData != null && debugData.users != null) {
+      debugData.users.remove(user);
+      return;
+    }
+    assert false;
   }
 
   public void removeDebugPhiUser(Phi user) {
-    debugData.phiUsers.remove(user);
+    if (debugData != null && debugData.phiUsers != null) {
+      debugData.phiUsers.remove(user);
+      return;
+    }
+    assert false;
   }
 
   public boolean hasUsersInfo() {
@@ -290,68 +362,27 @@
       return;
     }
     for (Instruction user : uniqueUsers()) {
-      user.inValues.replaceAll(v -> {
-        if (v == this) {
-          newValue.addUser(user);
-          return newValue;
-        }
-        return v;
-      });
+      user.replaceValue(this, newValue);
     }
     for (Phi user : uniquePhiUsers()) {
-      user.getOperands().replaceAll(v -> {
-        if (v == this) {
-          newValue.addPhiUser(user);
-          return newValue;
-        }
-        return v;
-      });
+      user.replaceOperand(this, newValue);
     }
     if (debugData != null) {
       for (Instruction user : debugUsers()) {
-        if (user.getDebugValues().remove(this)) {
-          user.addDebugValue(newValue);
-        }
+        user.replaceDebugValue(this, newValue);
       }
       for (Phi user : debugPhiUsers()) {
-        if (user.getDebugValues().remove(this)) {
-          user.addDebugValue(newValue);
-        }
+        user.replaceDebugValue(this, newValue);
       }
     }
     clearUsers();
   }
 
-  public void replaceInUsers(Value newValue) {
-    if (!uniqueUsers().isEmpty()) {
-      List<Instruction> toRemove = new ArrayList<>(uniqueUsers().size());
-      for (Instruction user : uniqueUsers()) {
-        user.replaceValue(this, newValue, toRemove);
-      }
-      toRemove.forEach(this::removeUser);
-    }
-    if (!uniquePhiUsers().isEmpty()) {
-      List<Phi> toRemove = new ArrayList<>(uniquePhiUsers().size());
-      for (Phi user : uniquePhiUsers()) {
-        user.replaceTrivialPhi(this, newValue, toRemove);
-      }
-      toRemove.forEach(this::removePhiUser);
-    }
-    if (debugData != null) {
-      if (!debugUsers().isEmpty()) {
-        List<Instruction> toRemove = new ArrayList<>(debugUsers().size());
-        for (Instruction user : debugUsers()) {
-          user.replaceDebugValue(this, newValue, toRemove);
-        }
-        toRemove.forEach(this::removeDebugUser);
-      }
-      if (!debugPhiUsers().isEmpty()) {
-        List<Phi> toRemove = new ArrayList<>(debugPhiUsers().size());
-        for (Phi user : debugPhiUsers()) {
-          user.replaceTrivialDebugPhi(this, newValue, toRemove);
-        }
-        toRemove.forEach(this::removeDebugPhiUser);
-      }
+  public void replaceDebugUser(Instruction oldUser, Instruction newUser) {
+    DebugUse use = debugData.users.remove(oldUser);
+    if (use != null) {
+      newUser.addDebugValue(this);
+      debugData.users.put(newUser, use);
     }
   }
 
@@ -583,4 +614,10 @@
     }
     return true;
   }
+
+  public boolean isZero() {
+    return isConstant()
+        && getConstInstruction().isConstNumber()
+        && getConstInstruction().asConstNumber().isZero();
+  }
 }
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 585260b..b32f34d 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
@@ -18,12 +18,11 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -32,8 +31,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -51,7 +50,7 @@
  * <p>
  * Recursive calls are not present.
  */
-public class CallGraph {
+public class CallGraph extends CallSiteInformation {
 
   private CallGraph(InternalOptions options) {
     this.shuffle = options.testing.irOrdering;
@@ -132,15 +131,7 @@
   }
 
   private final Map<DexEncodedMethod, Node> nodes = new LinkedHashMap<>();
-  private final Map<DexEncodedMethod, Set<DexEncodedMethod>> breakers = new HashMap<>();
-  private final Function<List<DexEncodedMethod>, List<DexEncodedMethod>> shuffle;
-
-  // Returns whether the method->callee edge has been removed from the call graph
-  // to break a cycle in the call graph.
-  public boolean isBreaker(DexEncodedMethod method, DexEncodedMethod callee) {
-    Set<DexEncodedMethod> value = breakers.get(method);
-    return (value != null) && value.contains(callee);
-  }
+  private final Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> shuffle;
 
   private Set<DexEncodedMethod> singleCallSite = Sets.newIdentityHashSet();
   private Set<DexEncodedMethod> doubleCallSite = Sets.newIdentityHashSet();
@@ -170,10 +161,12 @@
    * For pinned methods (methods kept through Proguard keep rules) this will always answer
    * <code>false</code>.
    */
+  @Override
   public boolean hasSingleCallSite(DexEncodedMethod method) {
     return singleCallSite.contains(method);
   }
 
+  @Override
   public boolean hasDoubleCallSite(DexEncodedMethod method) {
     return doubleCallSite.contains(method);
   }
@@ -211,12 +204,10 @@
    * All nodes in the graph are extracted if called repeatedly until null is returned.
    * Please note that there are no cycles in this graph (see {@link #breakCycles}).
    * <p>
-   *
-   * @return List of {@link DexEncodedMethod}.
    */
-  private List<DexEncodedMethod> extractLeaves() {
+  private Set<DexEncodedMethod> extractLeaves() {
     if (isEmpty()) {
-      return Collections.emptyList();
+      return Collections.emptySet();
     }
     // First identify all leaves before removing them from the graph.
     List<Node> leaves = nodes.values().stream().filter(Node::isLeaf).collect(Collectors.toList());
@@ -224,7 +215,8 @@
       leaf.callers.forEach(caller -> caller.callees.remove(leaf));
       nodes.remove(leaf.method);
     });
-    return shuffle.apply(leaves.stream().map(leaf -> leaf.method).collect(Collectors.toList()));
+    return shuffle.apply(leaves.stream().map(leaf -> leaf.method)
+        .collect(Collectors.toCollection(LinkedHashSet::new)));
   }
 
   private int traverse(Node node, Set<Node> stack, Set<Node> marked) {
@@ -246,8 +238,6 @@
           // We have a cycle; break it by removing node->callee.
           toBeRemoved.add(callee);
           callee.callers.remove(node);
-          breakers.computeIfAbsent(node.method,
-              ignore -> Sets.newIdentityHashSet()).add(callee.method);
         } else {
           numberOfCycles += traverse(callee, stack, marked);
         }
@@ -264,7 +254,6 @@
 
   private int breakCycles() {
     // Break cycles in this call graph by removing edges causing cycles.
-    // The remove edges are stored in @breakers.
     int numberOfCycles = 0;
     Set<Node> stack = Sets.newIdentityHashSet();
     Set<Node> marked = Sets.newIdentityHashSet();
@@ -294,16 +283,23 @@
     return nodes.size() == 0;
   }
 
+  /**
+   * Applies the given method to all leaf nodes of the graph.
+   * <p>
+   * As second parameter, a predicate that can be used to decide whether another method is
+   * processed at the same time is passed. This can be used to avoid races in concurrent processing.
+   */
   public <E extends Exception> void forEachMethod(
-      ThrowingConsumer<DexEncodedMethod, E> consumer, ExecutorService executorService)
+      ThrowingBiConsumer<DexEncodedMethod, Predicate<DexEncodedMethod>, E> consumer,
+      ExecutorService executorService)
       throws ExecutionException {
     while (!isEmpty()) {
-      List<DexEncodedMethod> methods = extractLeaves();
+      Set<DexEncodedMethod> methods = extractLeaves();
       assert methods.size() > 0;
       List<Future<?>> futures = new ArrayList<>();
       for (DexEncodedMethod method : methods) {
         futures.add(executorService.submit(() -> {
-          consumer.accept(method);
+          consumer.accept(method, methods::contains);
           return null; // we want a Callable not a Runnable to be able to throw
         }));
       }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
new file mode 100644
index 0000000..651fb6f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallSiteInformation.java
@@ -0,0 +1,38 @@
+// 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;
+
+public abstract class CallSiteInformation {
+
+  /**
+   * Check if the <code>method</code> is guaranteed to only have a single call site.
+   * <p>
+   * For pinned methods (methods kept through Proguard keep rules) this will always answer
+   * <code>false</code>.
+   */
+  public abstract boolean hasSingleCallSite(DexEncodedMethod method);
+
+  public abstract boolean hasDoubleCallSite(DexEncodedMethod method);
+
+  public static CallSiteInformation empty() {
+    return EmptyCallSiteInformation.EMPTY_INFO;
+  }
+
+  private static class EmptyCallSiteInformation extends CallSiteInformation {
+
+    private static EmptyCallSiteInformation EMPTY_INFO = new EmptyCallSiteInformation();
+
+    @Override
+    public boolean hasSingleCallSite(DexEncodedMethod method) {
+      return false;
+    }
+
+    @Override
+    public boolean hasDoubleCallSite(DexEncodedMethod method) {
+      return false;
+    }
+  }
+}
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 bc821d3..17b0e36 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
@@ -27,6 +27,7 @@
 import com.android.tools.r8.code.MoveResultWide;
 import com.android.tools.r8.code.SwitchPayload;
 import com.android.tools.r8.code.Throw;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexAccessFlags;
 import com.android.tools.r8.graph.DexCode;
@@ -40,6 +41,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.MoveType;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -162,6 +164,17 @@
   }
 
   @Override
+  public int getMoveExceptionRegister() {
+    // No register, move-exception is manually entered during construction.
+    return -1;
+  }
+
+  @Override
+  public DebugPosition getDebugPositionAtOffset(int offset) {
+    throw new Unreachable();
+  }
+
+  @Override
   public boolean verifyCurrentInstructionCanThrow() {
     return currentDexInstruction.canThrow();
   }
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 e10c690..245a75a 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
@@ -78,7 +78,9 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.code.Xor;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
@@ -93,6 +95,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -124,6 +127,15 @@
     }
   }
 
+  private static class MoveExceptionWorklistItem extends WorklistItem {
+    private final int targetOffset;
+
+    private MoveExceptionWorklistItem(BasicBlock block, int targetOffset) {
+      super(block, -1);
+      this.targetOffset = targetOffset;
+    }
+  }
+
   /**
    * Representation of lists of values that can be used as keys in maps. A list of
    * values is equal to another list of values if it contains exactly the same values
@@ -450,6 +462,11 @@
       setCurrentBlock(item.block);
       blocks.add(currentBlock);
       currentBlock.setNumber(nextBlockNumber++);
+      // Process synthesized move-exception block specially.
+      if (item instanceof MoveExceptionWorklistItem) {
+        processMoveExceptionItem((MoveExceptionWorklistItem) item);
+        continue;
+      }
       // Build IR for each dex instruction in the block.
       for (int i = item.firstInstructionIndex; i < source.instructionCount(); ++i) {
         if (currentBlock == null) {
@@ -468,6 +485,22 @@
     }
   }
 
+  private void processMoveExceptionItem(MoveExceptionWorklistItem moveExceptionItem) {
+    // TODO(zerny): Link with outer try-block handlers, if any. b/65203529
+    int moveExceptionDest = source.getMoveExceptionRegister();
+    assert moveExceptionDest >= 0;
+    int targetIndex = source.instructionIndex(moveExceptionItem.targetOffset);
+    Value out = writeRegister(moveExceptionDest, MoveType.OBJECT, ThrowingInfo.NO_THROW, null);
+    MoveException moveException = new MoveException(out);
+    moveException.setPosition(source.getDebugPositionAtOffset(moveExceptionItem.targetOffset));
+    currentBlock.add(moveException);
+    currentBlock.add(new Goto());
+    BasicBlock targetBlock = getTarget(moveExceptionItem.targetOffset);
+    currentBlock.link(targetBlock);
+    addToWorklist(targetBlock, targetIndex);
+    closeCurrentBlock();
+  }
+
   // Helper to resolve switch payloads and build switch instructions (dex code only).
   public void resolveAndBuildSwitch(int value, int fallthroughOffset, int payloadOffset) {
     source.resolveAndBuildSwitch(value, fallthroughOffset, payloadOffset, this);
@@ -723,7 +756,7 @@
   }
 
   public void addNullConst(int dest, long value) {
-    canonicalizeAndAddConst(ConstType.INT, dest, value, nullConstants);
+    canonicalizeAndAddConst(ConstType.OBJECT, dest, value, nullConstants);
   }
 
   public void addConstClass(int dest, DexType type) {
@@ -922,8 +955,7 @@
       throws ApiLevelException {
     if (type == Invoke.Type.POLYMORPHIC && !options.canUseInvokePolymorphic()) {
       throw new ApiLevelException(
-          Constants.ANDROID_O_API,
-          "Android O",
+          AndroidApiLevel.O,
           "MethodHandle.invoke and MethodHandle.invokeExact",
           null /* sourceString */);
     }
@@ -1590,9 +1622,7 @@
   // Private instruction helpers.
   private void addInstruction(Instruction ir) {
     attachLocalChanges(ir);
-    if (currentDebugPosition != null && !ir.isMoveException()) {
-      flushCurrentDebugPosition();
-    }
+    flushCurrentDebugPosition(ir);
     currentBlock.add(ir);
     if (ir.instructionTypeCanThrow()) {
       assert source.verifyCurrentInstructionCanThrow();
@@ -1601,18 +1631,33 @@
         assert !throwingInstructionInCurrentBlock;
         throwingInstructionInCurrentBlock = true;
         List<BasicBlock> targets = new ArrayList<>(catchHandlers.getAllTargets().size());
-        for (int targetOffset : catchHandlers.getAllTargets()) {
-          BasicBlock target = getTarget(targetOffset);
-          addToWorklist(target, source.instructionIndex(targetOffset));
-          targets.add(target);
+        int moveExceptionDest = source.getMoveExceptionRegister();
+        if (moveExceptionDest < 0) {
+          for (int targetOffset : catchHandlers.getAllTargets()) {
+            BasicBlock target = getTarget(targetOffset);
+            addToWorklist(target, source.instructionIndex(targetOffset));
+            targets.add(target);
+          }
+        } else {
+          // If there is a well-defined move-exception destination register (eg, compiling from
+          // Java-bytecode) then we construct move-exception header blocks for each unique target.
+          Map<BasicBlock, BasicBlock> moveExceptionHeaders =
+              new IdentityHashMap<>(catchHandlers.getUniqueTargets().size());
+          for (int targetOffset : catchHandlers.getAllTargets()) {
+            BasicBlock target = getTarget(targetOffset);
+            BasicBlock header = moveExceptionHeaders.get(target);
+            if (header == null) {
+              header = new BasicBlock();
+              header.incrementUnfilledPredecessorCount();
+              moveExceptionHeaders.put(target, header);
+              ssaWorklist.add(new MoveExceptionWorklistItem(header, targetOffset));
+            }
+            targets.add(header);
+          }
         }
         currentBlock.linkCatchSuccessors(catchHandlers.getGuards(), targets);
       }
     }
-    if (currentDebugPosition != null) {
-      assert ir.isMoveException();
-      flushCurrentDebugPosition();
-    }
   }
 
   private void attachLocalChanges(Instruction ir) {
@@ -1789,6 +1834,26 @@
         assert returnType == MoveType.fromDexType(method.method.proto.returnType);
       }
       closeCurrentBlock();
+
+      // Collect the debug values which are live on all returns.
+      Set<Value> debugValuesForReturn = Sets.newIdentityHashSet();
+      for (Value value : exitBlocks.get(0).exit().getDebugValues()) {
+        boolean include = true;
+        for (int i = 1; i < exitBlocks.size() && include; i++) {
+          include = exitBlocks.get(i).exit().getDebugValues().contains(value);
+        }
+        if (include) {
+          debugValuesForReturn.add(value);
+        }
+      }
+
+      // Move all these debug values to the new return.
+      for (Value value : debugValuesForReturn) {
+        for (BasicBlock block : exitBlocks) {
+          block.exit().moveDebugValue(value, normalExitBlock.exit());
+        }
+      }
+
       // Replace each return instruction with a goto to the new exit block.
       List<Value> operands = new ArrayList<>();
       for (BasicBlock block : exitBlocks) {
@@ -1800,12 +1865,7 @@
         }
         Goto gotoExit = new Goto();
         gotoExit.setBlock(block);
-        if (options.debug) {
-          for (Value value : ret.getDebugValues()) {
-            gotoExit.addDebugValue(value);
-            value.removeDebugUser(ret);
-          }
-        }
+        ret.moveDebugValues(gotoExit);
         instructions.set(instructions.size() - 1, gotoExit);
         block.link(normalExitBlock);
         gotoExit.setTarget(normalExitBlock);
@@ -2005,26 +2065,23 @@
 
   public void updateCurrentDebugPosition(int line, DexString file) {
     // Stack-trace support requires position information in both debug and release mode.
-    flushCurrentDebugPosition();
+    flushCurrentDebugPosition(null);
     currentDebugPosition = new DebugPosition(line, file);
     attachLocalChanges(currentDebugPosition);
   }
 
-  private void flushCurrentDebugPosition() {
+  private void flushCurrentDebugPosition(Instruction instruction) {
     if (currentDebugPosition != null) {
       DebugPosition position = currentDebugPosition;
       currentDebugPosition = null;
-      if (!currentBlock.getInstructions().isEmpty()) {
-        MoveException move = currentBlock.getInstructions().getLast().asMoveException();
-        if (move != null && move.getPosition() == null) {
-          // Set the position on the move-exception instruction.
-          // ART/DX associates the move-exception pc with the catch-declaration line.
-          // See debug.ExceptionTest.testStepOnCatch().
-          move.setPosition(position);
-          return;
-        }
+      if (instruction != null && instruction.isMoveException()) {
+        // Set the position on the move-exception instruction.
+        // ART/DX associates the move-exception pc with the catch-declaration line.
+        // See debug.ExceptionTest.testStepOnCatch().
+        instruction.asMoveException().setPosition(position);
+      } else {
+        addInstruction(position);
       }
-      addInstruction(position);
     }
   }
 
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 82cc92f..5b5d382 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
@@ -43,12 +43,12 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.function.BiConsumer;
+import java.util.function.Predicate;
 
 public class IRConverter {
 
@@ -68,7 +68,6 @@
   private final LensCodeRewriter lensCodeRewriter;
   private final Inliner inliner;
   private final ProtoLitePruner protoLiteRewriter;
-  private CallGraph callGraph;
 
   private OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
   private DexString highestSortingString;
@@ -241,9 +240,6 @@
               }));
     }
     ThreadUtils.awaitFutures(futures);
-
-    // Get rid of <clinit> methods with no code.
-    removeEmptyClassInitializers();
   }
 
   void convertMethodToDex(DexEncodedMethod method) throws ApiLevelException {
@@ -251,7 +247,9 @@
       boolean matchesMethodFilter = options.methodMatchesFilter(method);
       if (matchesMethodFilter) {
         if (method.getCode().isJarCode()) {
-          rewriteCode(method, ignoreOptimizationFeedback, Outliner::noProcessing);
+          // We do not process in call graph order, so anything could be a leaf.
+          rewriteCode(method, ignoreOptimizationFeedback, x -> true, CallSiteInformation.empty(),
+              Outliner::noProcessing);
         }
         updateHighestSortingStrings(method);
       }
@@ -271,10 +269,6 @@
       throws ExecutionException, ApiLevelException {
     removeLambdaDeserializationMethods();
 
-    timing.begin("Build call graph");
-    callGraph = CallGraph.build(application, appInfo.withSubtyping(), graphLense, options);
-    timing.end();
-
     // The process is in two phases.
     // 1) Subject all DexEncodedMethods to optimization (except outlining).
     //    - a side effect is candidates for outlining are identified.
@@ -282,16 +276,19 @@
     // Ideally, we should outline eagerly when threshold for a template has been reached.
 
     // Process the application identifying outlining candidates.
-    timing.begin("IR conversion phase 1");
     OptimizationFeedback directFeedback = new OptimizationFeedbackDirect();
-    callGraph.forEachMethod(method -> {
-          processMethod(method, directFeedback,
-              outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
-    }, executorService);
-    timing.end();
-
-    // Get rid of <clinit> methods with no code.
-    removeEmptyClassInitializers();
+    {
+      timing.begin("Build call graph");
+      CallGraph callGraph = CallGraph
+          .build(application, appInfo.withSubtyping(), graphLense, options);
+      timing.end();
+      timing.begin("IR conversion phase 1");
+      callGraph.forEachMethod((method, isProcessedConcurrently) -> {
+        processMethod(method, directFeedback, isProcessedConcurrently, callGraph,
+            outliner == null ? Outliner::noProcessing : outliner::identifyCandidates);
+      }, executorService);
+      timing.end();
+    }
 
     // Build a new application with jumbo string info.
     Builder builder = new Builder(application);
@@ -313,16 +310,17 @@
       if (outlineClass != null) {
         // We need a new call graph to ensure deterministic order and also processing inside out
         // to get maximal inlining. Use a identity lense, as the code has been rewritten.
-        callGraph = CallGraph
+        CallGraph callGraph = CallGraph
             .build(application, appInfo.withSubtyping(), GraphLense.getIdentityLense(), options);
         Set<DexEncodedMethod> outlineMethods = outliner.getMethodsSelectedForOutlining();
-        callGraph.forEachMethod(method -> {
+        callGraph.forEachMethod((method, isProcessedConcurrently) -> {
           if (!outlineMethods.contains(method)) {
             return;
           }
           // This is the second time we compile this method first mark it not processed.
           assert !method.getCode().isOutlineCode();
-          processMethod(method, ignoreOptimizationFeedback, outliner::applyOutliningCandidate);
+          processMethod(method, ignoreOptimizationFeedback, isProcessedConcurrently, callGraph,
+              outliner::applyOutliningCandidate);
           assert method.isProcessed();
         }, executorService);
         builder.addSynthesizedClass(outlineClass, true);
@@ -334,16 +332,6 @@
     return builder.build();
   }
 
-  private void removeEmptyClassInitializers() {
-    application.classes().forEach(this::removeEmptyClassInitializer);
-  }
-
-  private void removeEmptyClassInitializer(DexProgramClass clazz) {
-    if (clazz.hasTrivialClassInitializer()) {
-      clazz.removeStaticMethod(clazz.getClassInitializer());
-    }
-  }
-
   private void clearDexMethodCompilationState() {
     application.classes().forEach(this::clearDexMethodCompilationState);
   }
@@ -405,7 +393,8 @@
 
   public void optimizeSynthesizedMethod(DexEncodedMethod method) throws ApiLevelException {
     // Process the generated method, but don't apply any outlining.
-    processMethod(method, ignoreOptimizationFeedback, Outliner::noProcessing);
+    processMethod(method, ignoreOptimizationFeedback, x -> false, CallSiteInformation.empty(),
+        Outliner::noProcessing);
   }
 
   private String logCode(InternalOptions options, DexEncodedMethod method) {
@@ -414,12 +403,14 @@
 
   public void processMethod(DexEncodedMethod method,
       OptimizationFeedback feedback,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler)
           throws ApiLevelException {
     Code code = method.getCode();
     boolean matchesMethodFilter = options.methodMatchesFilter(method);
     if (code != null && matchesMethodFilter) {
-      rewriteCode(method, feedback, outlineHandler);
+      rewriteCode(method, feedback, isProcessedConcurrently, callSiteInformation, outlineHandler);
     } else {
       // Mark abstract methods as processed as well.
       method.markProcessed(Constraint.NEVER);
@@ -428,6 +419,8 @@
 
   private void rewriteCode(DexEncodedMethod method,
       OptimizationFeedback feedback,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler)
       throws ApiLevelException {
     if (options.verbose) {
@@ -473,7 +466,7 @@
     if (options.inlineAccessors && inliner != null) {
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       assert !options.debug;
-      inliner.performInlining(method, code, callGraph);
+      inliner.performInlining(method, code, isProcessedConcurrently, callSiteInformation);
     }
     codeRewriter.removeCastChains(code);
     codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
@@ -484,7 +477,9 @@
     codeRewriter.foldConstants(code);
     codeRewriter.rewriteSwitch(code);
     codeRewriter.simplifyIf(code);
-    codeRewriter.collectClassInitializerDefaults(method, code);
+    if (!options.debug) {
+      codeRewriter.collectClassInitializerDefaults(method, code);
+    }
     if (Log.ENABLED) {
       Log.debug(getClass(), "Intermediate (SSA) flow graph for %s:\n%s",
           method.toSourceString(), code);
@@ -560,7 +555,6 @@
     LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
     registerAllocator.allocateRegisters(options.debug);
     printMethod(code, "After register allocation (non-SSA)");
-    printLiveRanges(registerAllocator, "Final live ranges.");
     if (!options.debug) {
       CodeRewriter.removedUnneededDebugPositions(code);
     }
@@ -595,10 +589,4 @@
       printer.end("cfg");
     }
   }
-
-  private void printLiveRanges(LinearScanRegisterAllocator allocator, String title) {
-    if (printer != null) {
-      allocator.print(printer, title);
-    }
-  }
 }
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 668e220..2ab087c 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
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.Cmp.Bias;
 import com.android.tools.r8.ir.code.ConstType;
+import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.MemberType;
@@ -35,6 +36,7 @@
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.utils.ThrowingBiConsumer;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.io.PrintWriter;
@@ -136,10 +138,10 @@
       "([Ljava/lang/Object;)Ljava/lang/Object;";
 
   // Language types.
-  private static final Type CLASS_TYPE = Type.getObjectType("java/lang/Class");
-  private static final Type STRING_TYPE = Type.getObjectType("java/lang/String");
-  private static final Type INT_ARRAY_TYPE = Type.getObjectType(INT_ARRAY_DESC);
-  private static final Type THROWABLE_TYPE = Type.getObjectType("java/lang/Throwable");
+  static final Type CLASS_TYPE = Type.getObjectType("java/lang/Class");
+  static final Type STRING_TYPE = Type.getObjectType("java/lang/String");
+  static final Type INT_ARRAY_TYPE = Type.getObjectType(INT_ARRAY_DESC);
+  static final Type THROWABLE_TYPE = Type.getObjectType("java/lang/Throwable");
 
   private static final int[] NO_TARGETS = {};
 
@@ -264,6 +266,7 @@
     // Record types for arguments.
     Int2ReferenceMap<MoveType> argumentLocals = recordArgumentTypes();
     Int2ReferenceMap<MoveType> initializedLocals = new Int2ReferenceOpenHashMap<>(argumentLocals);
+    Int2ReferenceMap<ConstType> uninitializedLocals = new Int2ReferenceOpenHashMap<>();
     // Initialize all non-argument locals to ensure safe insertion of debug-local instructions.
     for (Object o : node.localVariables) {
       LocalVariableNode local = (LocalVariableNode) o;
@@ -292,20 +295,34 @@
         int localRegister2 = state.writeLocal(local.index, localType);
         assert localRegister == localRegister2;
         initializedLocals.put(localRegister, moveType(localType));
-        builder.addDebugUninitialized(localRegister, constType(localType));
+        uninitializedLocals.put(localRegister, constType(localType));
       }
     }
+
+    // TODO(zerny): This is getting a little out of hands. Clean it up.
+
     // Add debug information for all locals at the initial label.
+    List<Local> locals = null;
     if (initialLabel != null) {
-      for (Local local : state.openLocals(initialLabel)) {
+      locals = state.openLocals(initialLabel);
+    }
+
+    // Build the actual argument instructions now that type and debug information is known
+    // for arguments.
+    buildArgumentInstructions(builder);
+
+    for (Entry<ConstType> entry : uninitializedLocals.int2ReferenceEntrySet()) {
+      builder.addDebugUninitialized(entry.getIntKey(), entry.getValue());
+    }
+
+    if (locals != null) {
+      for (Local local : locals) {
         if (!argumentLocals.containsKey(local.slot.register)) {
           builder.addDebugLocalStart(local.slot.register, local.info);
         }
       }
     }
-    // Build the actual argument instructions now that type and debug information is known
-    // for arguments.
-    buildArgumentInstructions(builder);
+
     if (isSynchronized()) {
       generatingMethodSynchronization = true;
       Type clazzType = Type.getType(clazz.toDescriptorString());
@@ -376,14 +393,6 @@
     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
@@ -441,10 +450,8 @@
   private void buildExceptionalPostlude(IRBuilder builder) {
     assert isSynchronized();
     generatingMethodSynchronization = true;
-    int exceptionRegister = 0; // We are exiting the method so we just overwrite register 0.
-    builder.addMoveException(exceptionRegister);
     buildMonitorExit(builder);
-    builder.addThrow(exceptionRegister);
+    builder.addThrow(getMoveExceptionRegister());
     generatingMethodSynchronization = false;
   }
 
@@ -474,14 +481,6 @@
     // 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++) {
-        TryCatchBlockNode tryCatchBlockNode = (TryCatchBlockNode) node.tryCatchBlocks.get(i);
-        if (tryCatchBlockNode.handler == insn) {
-          builder.addMoveException(state.push(THROWABLE_TYPE));
-          break;
-        }
-      }
     }
 
     String preInstructionState;
@@ -552,6 +551,11 @@
   }
 
   @Override
+  public int getMoveExceptionRegister() {
+    return state.startOfStack;
+  }
+
+  @Override
   public boolean verifyCurrentInstructionCanThrow() {
     return generatingMethodSynchronization || canThrow(currentInstruction);
   }
@@ -2291,8 +2295,19 @@
     }
   }
 
+  private boolean isExitingThrow(InsnNode insn) {
+    List<TryCatchBlock> handlers = getTryHandlers(insn);
+    if (handlers.isEmpty()) {
+      return true;
+    }
+    if (!isSynchronized() || handlers.size() > 1) {
+      return false;
+    }
+    return handlers.get(0) == EXCEPTIONAL_SYNC_EXIT;
+  }
+
   private void addThrow(InsnNode insn, int register, IRBuilder builder) {
-    if (getTryHandlers(insn).isEmpty()) {
+    if (isExitingThrow(insn)) {
       processLocalVariablesAtExit(insn, builder);
     } else {
       processLocalVariablesAtControlEdge(insn, builder);
@@ -2653,8 +2668,10 @@
       case Opcodes.H_PUTSTATIC:
         return MethodHandleType.STATIC_PUT;
       case Opcodes.H_INVOKESPECIAL:
+        assert !handle.getName().equals(Constants.INSTANCE_INITIALIZER_NAME);
+        assert !handle.getName().equals(Constants.CLASS_INITIALIZER_NAME);
         DexType owner = application.getTypeFromName(handle.getOwner());
-        if (owner == clazz || handle.getName().equals(Constants.INSTANCE_INITIALIZER_NAME)) {
+        if (owner == clazz) {
           return MethodHandleType.INVOKE_DIRECT;
         } else {
           return MethodHandleType.INVOKE_SUPER;
@@ -2817,6 +2834,23 @@
     builder.updateCurrentDebugPosition(insn.line, null);
   }
 
+  @Override
+  public DebugPosition getDebugPositionAtOffset(int offset) {
+    int index = instructionIndex(offset);
+    if (index < 0 || instructionCount() <= index) {
+      return null;
+    }
+    AbstractInsnNode insn = node.instructions.get(index);
+    if (insn instanceof LabelNode) {
+      insn = insn.getNext();
+    }
+    if (insn instanceof LineNumberNode) {
+      LineNumberNode line = (LineNumberNode) insn;
+      return new DebugPosition(line.line, null);
+    }
+    return null;
+  }
+
   // Printing helpers.
 
   @Override
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 03c614a..12b62ad 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
@@ -134,8 +134,7 @@
       return (sort == Type.OBJECT && otherSort == Type.ARRAY)
           || (sort == Type.ARRAY && otherSort == Type.OBJECT)
           || (sort == Type.OBJECT && otherSort == Type.OBJECT)
-          || (sort == Type.ARRAY && otherSort == Type.ARRAY
-              && isReferenceCompatible(getArrayElementType(type), getArrayElementType(other)));
+          || (sort == Type.ARRAY && otherSort == Type.ARRAY);
     }
   }
 
@@ -166,7 +165,7 @@
     }
   }
 
-  private final int startOfStack;
+  final int startOfStack;
   private int topOfStack;
 
   // Locals are split into three parts based on types:
@@ -344,6 +343,11 @@
 
   private Local setLocalInfoForRegister(int register, DebugLocalInfo info) {
     Local existingLocal = getLocalForRegister(register);
+    Type type = Type.getType(info.type.toDescriptorString());
+    if (!existingLocal.slot.isCompatibleWith(type)) {
+      throw new InvalidDebugInfoException(
+          "Attempt to define local of type " + prettyType(existingLocal.slot.type) + " as " + info);
+    }
     Local local = new Local(existingLocal.slot, info);
     locals[register] = local;
     return local;
@@ -420,12 +424,7 @@
 
   public Slot pop(Type type) {
     Slot slot = pop();
-    boolean compatible = slot.isCompatibleWith(type);
-    if (!compatible && !localVariables.isEmpty()) {
-      throw new InvalidDebugInfoException("Expected to read stack value of type " + prettyType(type)
-          + " but found value of type " + prettyType(slot.type));
-    }
-    assert compatible;
+    assert slot.isCompatibleWith(type);
     return slot;
   }
 
@@ -464,7 +463,10 @@
   }
 
   public boolean recordStateForExceptionalTarget(int target, JarSourceCode source) {
-    return recordStateForTarget(target, locals.clone(), ImmutableList.of(), source);
+    return recordStateForTarget(target,
+        locals.clone(),
+        ImmutableList.of(new Slot(startOfStack, JarSourceCode.THROWABLE_TYPE)),
+        source);
   }
 
   private boolean recordStateForTarget(int target, Local[] locals, ImmutableList<Slot> stack,
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 cf1e669..16e124a 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
@@ -7,6 +7,7 @@
 import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DebugPosition;
 
 /**
  * Abstraction of the input/source code for the IRBuilder.
@@ -27,6 +28,8 @@
 
   DebugLocalInfo getCurrentLocal(int register);
 
+  DebugPosition getDebugPositionAtOffset(int offset);
+
   /**
    * Trace block structure of the source-program.
    *
@@ -58,6 +61,7 @@
   void resolveAndBuildNewArrayFilledData(int arrayRef, int payloadOffset, IRBuilder builder);
 
   CatchHandlers<Integer> getCurrentCatchHandlers();
+  int getMoveExceptionRegister();
 
   // For debugging/verification purpose.
   boolean verifyRegister(int register);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
index e322717..bba7b23 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaDescriptor.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
-import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
index 9f833f0..8820c61 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.google.common.base.Equivalence;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.List;
 
 class BasicBlockInstructionsEquivalence extends Equivalence<BasicBlock> {
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 d44ecd9..8ae2147 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
@@ -64,27 +64,32 @@
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.SwitchUtils.EnumSwitchInfo;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.LongInterval;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.IntLists;
 import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -344,11 +349,25 @@
   }
 
   // TODO(sgjesse); Move this somewhere else, and reuse it for some of the other switch rewritings.
-  public static class SwitchBuilder {
+  public abstract static class InstructionBuilder<T> {
+    protected int blockNumber;
+
+    public abstract T self();
+
+    public T setBlockNumber(int blockNumber) {
+      this.blockNumber = blockNumber;
+      return self();
+    }
+  }
+
+  public static class SwitchBuilder extends InstructionBuilder<SwitchBuilder> {
     private Value value;
     private Int2ObjectSortedMap<BasicBlock> keyToTarget = new Int2ObjectAVLTreeMap<>();
     private BasicBlock fallthrough;
-    private int blockNumber;
+
+    public SwitchBuilder self() {
+      return this;
+    }
 
     public SwitchBuilder setValue(Value value) {
       this.value = value;
@@ -365,11 +384,6 @@
       return this;
     }
 
-    public SwitchBuilder setBlockNumber(int blockNumber) {
-      this.blockNumber = blockNumber;
-      return  this;
-    }
-
     public BasicBlock build() {
       final int NOT_FOUND = -1;
       Object2IntMap<BasicBlock> targetToSuccessorIndex = new Object2IntLinkedOpenHashMap<>();
@@ -400,65 +414,134 @@
     }
   }
 
+  public static class IfBuilder extends InstructionBuilder<IfBuilder> {
+    private final IRCode code;
+    private Value left;
+    private int right;
+    private BasicBlock target;
+    private BasicBlock fallthrough;
+    private int blockNumber;
+
+    public IfBuilder(IRCode code) {
+      this.code = code;
+    }
+
+    public IfBuilder self() {
+      return this;
+    }
+
+    public IfBuilder setLeft(Value left) {
+      this.left = left;
+      return  this;
+    }
+
+    public IfBuilder setRight(int right) {
+      this.right = right;
+      return  this;
+    }
+
+    public IfBuilder setTarget(BasicBlock target) {
+      this.target = target;
+      return this;
+    }
+
+    public IfBuilder setFallthrough(BasicBlock fallthrough) {
+      this.fallthrough = fallthrough;
+      return this;
+    }
+
+    public BasicBlock build() {
+      assert target != null;
+      assert fallthrough != null;
+      If newIf;
+      BasicBlock ifBlock;
+      if (right != 0) {
+        ConstNumber rightConst = code.createIntConstant(right);
+        newIf = new If(Type.EQ, ImmutableList.of(left, rightConst.dest()));
+        ifBlock = BasicBlock.createIfBlock(blockNumber, newIf, rightConst);
+      } else {
+        newIf = new If(Type.EQ, left);
+        ifBlock = BasicBlock.createIfBlock(blockNumber, newIf);
+      }
+      ifBlock.link(target);
+      ifBlock.link(fallthrough);
+      return ifBlock;
+    }
+  }
+
   /**
    * Covert the switch instruction to a sequence of if instructions checking for a specified
    * set of keys, followed by a new switch with the remaining keys.
    */
   private void convertSwitchToSwitchAndIfs(
       IRCode code, ListIterator<BasicBlock> blocksIterator, BasicBlock originalBlock,
-      InstructionListIterator iterator, Switch theSwitch, List<Integer> keysToRemove) {
+      InstructionListIterator iterator, Switch theSwitch,
+      List<IntList> switches, IntList keysToRemove) {
+
+    // Extract the information from the switch before removing it.
+    Int2ReferenceSortedMap<BasicBlock> keyToTarget = theSwitch.getKeyToTargetMap();
+    Set<Value> originalDebugValues = ImmutableSet.copyOf(theSwitch.getDebugValues());
+
+    // Keep track of the current fallthrough, starting with the original.
+    BasicBlock fallthroughBlock = theSwitch.fallthroughBlock();
+
     // Split the switch instruction into its own block and remove it.
     iterator.previous();
     BasicBlock originalSwitchBlock = iterator.split(code, blocksIterator);
     assert !originalSwitchBlock.hasCatchHandlers();
     assert originalSwitchBlock.getInstructions().size() == 1;
     blocksIterator.remove();
-
-    int nextBlockNumber = code.getHighestBlockNumber() + 1;
-
-    // Collect targets for the keys to peel off, and create a new switch instruction without
-    // these keys.
-    SwitchBuilder switchBuilder = new SwitchBuilder();
-    List<BasicBlock> peeledOffTargets = new ArrayList<>();
-    for (int i = 0; i < theSwitch.numberOfKeys(); i++) {
-      BasicBlock target = theSwitch.targetBlock(i);
-      if (!keysToRemove.contains(theSwitch.getKey(i))) {
-        switchBuilder.addKeyAndTarget(theSwitch.getKey(i), theSwitch.targetBlock(i));
-      } else {
-        peeledOffTargets.add(target);
-      }
-    }
-    assert peeledOffTargets.size() == keysToRemove.size();
-    switchBuilder.setValue(theSwitch.value());
-    switchBuilder.setFallthrough(theSwitch.fallthroughBlock());
-    switchBuilder.setBlockNumber(nextBlockNumber++);
     theSwitch.getBlock().detachAllSuccessors();
     BasicBlock block = theSwitch.getBlock().unlinkSinglePredecessor();
     assert theSwitch.getBlock().getPredecessors().size() == 0;
     assert theSwitch.getBlock().getSuccessors().size() == 0;
     assert block == originalBlock;
 
-    BasicBlock newSwitchBlock = switchBuilder.build();
+    // Collect the new blocks for adding to the block list.
+    int nextBlockNumber = code.getHighestBlockNumber() + 1;
+    LinkedList<BasicBlock> newBlocks = new LinkedList<>();
 
-    // Create if blocks for each of the peeled off keys, and link them into the graph.
-    BasicBlock predecessor = originalBlock;
-    for (int i = 0; i < keysToRemove.size(); i++) {
-      int key = keysToRemove.get(i);
-      BasicBlock peeledOffTarget = peeledOffTargets.get(i);
-      ConstNumber keyConst = code.createIntConstant(key);
-      If theIf = new If(Type.EQ, ImmutableList.of(keyConst.dest(), theSwitch.value()));
-      BasicBlock ifBlock = BasicBlock.createIfBlock(nextBlockNumber++, theIf, keyConst);
-      predecessor.link(ifBlock);
-      ifBlock.link(peeledOffTarget);
-      predecessor = ifBlock;
-      blocksIterator.add(ifBlock);
-      assert !peeledOffTarget.getPredecessors().contains(theSwitch.getBlock());
+    // Build the switch-blocks backwards, to always have the fallthrough block in hand.
+    for (int i = switches.size() - 1; i >= 0; i--) {
+      SwitchBuilder switchBuilder = new SwitchBuilder();
+      switchBuilder.setValue(theSwitch.value());
+      IntList keys = switches.get(i);
+      for (int j = 0; j < keys.size(); j++) {
+        int key = keys.getInt(j);
+        switchBuilder.addKeyAndTarget(key, keyToTarget.get(key));
+      }
+      switchBuilder
+          .setFallthrough(fallthroughBlock)
+          .setBlockNumber(nextBlockNumber++);
+      BasicBlock newSwitchBlock = switchBuilder.build();
+      newBlocks.addFirst(newSwitchBlock);
+      fallthroughBlock = newSwitchBlock;
     }
-    predecessor.link(newSwitchBlock);
-    blocksIterator.add(newSwitchBlock);
 
-    // The switch fallthrough block is still the same, and it is right after the new switch block.
-    assert newSwitchBlock.exit().fallthroughBlock() == IteratorUtils.peekNext(blocksIterator);
+    // Build the if-blocks backwards, to always have the fallthrough block in hand.
+    for (int i = keysToRemove.size() - 1; i >= 0; i--) {
+      int key = keysToRemove.getInt(i);
+      BasicBlock peeledOffTarget = keyToTarget.get(key);
+      IfBuilder ifBuilder = new IfBuilder(code);
+      ifBuilder
+          .setLeft(theSwitch.value())
+          .setRight(key)
+          .setTarget(peeledOffTarget)
+          .setFallthrough(fallthroughBlock)
+          .setBlockNumber(nextBlockNumber++);
+      BasicBlock ifBlock = ifBuilder.build();
+      newBlocks.addFirst(ifBlock);
+      fallthroughBlock = ifBlock;
+    }
+
+    // Attach the debug values from the original switch to the first of the new instructions,
+    originalDebugValues.forEach(fallthroughBlock.exit()::addDebugValue);
+
+    // Finally link the block before the original switch to the new block sequence.
+    originalBlock.link(fallthroughBlock);
+
+    // Finally add the blocks.
+    newBlocks.forEach(blocksIterator::add);
   }
 
   public void rewriteSwitch(IRCode code) {
@@ -490,16 +573,16 @@
             }
           } else {
             // Split keys into outliers and sequences.
-            List<List<Integer>> sequences = new ArrayList<>();
-            List<Integer> outliers = new ArrayList<>();
+            List<IntList> sequences = new ArrayList<>();
+            IntList outliers = new IntArrayList();
 
-            List<Integer> current = new ArrayList<>();
+            IntList current = new IntArrayList();
             int[] keys = theSwitch.getKeys();
             int previousKey = keys[0];
             current.add(previousKey);
             for (int i = 1; i < keys.length; i++) {
               assert current.size() > 0;
-              assert current.get(current.size() - 1) == previousKey;
+              assert current.getInt(current.size() - 1) == previousKey;
               int key = keys[i];
               if (((long) key - (long) previousKey) > 1) {
                 if (current.size() == 1) {
@@ -507,7 +590,7 @@
                 } else {
                   sequences.add(current);;
                 }
-                current = new ArrayList<>();
+                current = new IntArrayList();
               }
               current.add(key);
               previousKey = key;
@@ -518,22 +601,45 @@
               sequences.add(current);
             }
 
-            // Only check for rewrite if there is one sequence and one or two outliers.
-            if (sequences.size() == 1 && outliers.size() <= 2) {
-              // Get the existing dex size for the payload (excluding the switch itself).
-              long currentSize = Switch.payloadSize(keys);
-              // Estimate the dex size of the rewritten payload and the additional if instructions.
-              long rewrittenSize = Switch.payloadSize(sequences.get(0));
+            // Get the existing dex size for the payload and switch.
+            long currentSize = Switch.payloadSize(keys) + Switch.estimatedDexSize();
+
+            // Never replace with more than 10 if/switch instructions.
+            if (outliers.size() + sequences.size() <= 10) {
+              // Calculate estimated size for splitting into ifs and switches.
+              long rewrittenSize = 0;
               for (Integer outlier : outliers) {
-                rewrittenSize += ConstNumber.estimatedDexSize(
-                    ConstType.fromMoveType(theSwitch.value().outType()), outlier);
+                if (outlier != 0) {
+                  rewrittenSize += ConstNumber.estimatedDexSize(
+                      ConstType.fromMoveType(theSwitch.value().outType()), outlier);
+                }
                 rewrittenSize += If.estimatedDexSize();
               }
-              // Rewrite if smaller.
+              for (List<Integer> sequence : sequences) {
+                rewrittenSize += Switch.payloadSize(sequence);
+              }
+              rewrittenSize += Switch.estimatedDexSize() * sequences.size();
+
               if (rewrittenSize < currentSize) {
                 convertSwitchToSwitchAndIfs(
-                    code, blocksIterator, block, iterator, theSwitch, outliers);
-                assert code.isConsistentSSA();
+                    code, blocksIterator, block, iterator, theSwitch, sequences, outliers);
+              }
+            } else if (outliers.size() > 1) {
+              // Calculate estimated size for splitting into switches (packed for the sequences
+              // and sparse for the outliers).
+              long rewrittenSize = 0;
+              for (List<Integer> sequence : sequences) {
+                rewrittenSize += Switch.payloadSize(sequence);
+              }
+              rewrittenSize += Switch.payloadSize(outliers);
+              rewrittenSize += Switch.estimatedDexSize() * (sequences.size() + 1);
+
+              if (rewrittenSize < currentSize) {
+                // Create a copy to not modify sequences.
+                List<IntList> seqs = new ArrayList<>(sequences);
+                seqs.add(outliers);
+                convertSwitchToSwitchAndIfs(
+                    code, blocksIterator, block, iterator, theSwitch, seqs, IntLists.EMPTY_LIST);
               }
             }
           }
@@ -722,7 +828,7 @@
                   assert (invoke.outType() == argument.outType()) ||
                       (invoke.outType() == MoveType.OBJECT
                           && argument.outType() == MoveType.SINGLE
-                          && argument.getConstInstruction().asConstNumber().isZero());
+                          && argument.isZero());
                   invoke.outValue().replaceUsers(argument);
                   invoke.setOutValue(null);
                 }
@@ -834,7 +940,8 @@
     if (exit == null) {
       return;
     }
-    Map<DexField, StaticPut> puts = Maps.newIdentityHashMap();
+    Set<StaticPut> puts = Sets.newIdentityHashSet();
+    Map<DexField, StaticPut> dominatingPuts = Maps.newIdentityHashMap();
     for (BasicBlock block : dominatorTree.dominatorBlocks(exit)) {
       InstructionListIterator iterator = block.listIterator(block.getInstructions().size());
       while (iterator.hasPrevious()) {
@@ -845,25 +952,29 @@
           if (field.getHolder() == method.method.getHolder()) {
             if (put.inValue().isConstant()) {
               if ((field.type.isClassType() || field.type.isArrayType())
-                  && put.inValue().getConstInstruction().isConstNumber() &&
-                  put.inValue().getConstInstruction().asConstNumber().isZero()) {
+                  && put.inValue().isZero()) {
                 // Collect put of zero as a potential default value.
-                puts.put(put.getField(), put);
+                dominatingPuts.putIfAbsent(put.getField(), put);
+                puts.add(put);
               } else if (field.type.isPrimitiveType() || field.type == dexItemFactory.stringType) {
                 // Collect put as a potential default value.
-                puts.put(put.getField(), put);
+                dominatingPuts.putIfAbsent(put.getField(), put);
+                puts.add(put);
               }
             } else if (isClassNameConstant(method, put)) {
               // Collect put of class name constant as a potential default value.
-              puts.put(put.getField(), put);
+              dominatingPuts.putIfAbsent(put.getField(), put);
+              puts.add(put);
             }
           }
         }
         if (current.isStaticGet()) {
           // If a static field is read, any collected potential default value cannot be a
           // default value.
-          if (puts.containsKey(current.asStaticGet().getField())) {
-            puts.remove(current.asStaticGet().getField());
+          DexField field = current.asStaticGet().getField();
+          if (dominatingPuts.containsKey(field)) {
+            dominatingPuts.remove(field);
+            Iterables.removeIf(puts, put -> put.getField() == field);
           }
         }
       }
@@ -871,13 +982,13 @@
 
     if (!puts.isEmpty()) {
       // Set initial values for static fields from the definitive static put instructions collected.
-      for (StaticPut put : puts.values()) {
+      for (StaticPut put : dominatingPuts.values()) {
         DexField field = put.getField();
         DexEncodedField encodedField = appInfo.definitionFor(field);
         if (field.type == dexItemFactory.stringType) {
           if (put.inValue().isConstant()) {
-            if (put.inValue().getConstInstruction().isConstNumber()) {
-              assert put.inValue().getConstInstruction().asConstNumber().isZero();
+            if (put.inValue().isConstNumber()) {
+              assert put.inValue().isZero();
               encodedField.staticValue = DexValueNull.NULL;
             } else {
               ConstString cnst = put.inValue().getConstInstruction().asConstString();
@@ -896,8 +1007,7 @@
             }
           }
         } else if (field.type.isClassType() || field.type.isArrayType()) {
-          if (put.inValue().getConstInstruction().isConstNumber()
-              && put.inValue().getConstInstruction().asConstNumber().isZero()) {
+          if (put.inValue().isZero()) {
             encodedField.staticValue = DexValueNull.NULL;
           } else {
             throw new Unreachable("Unexpected default value for field type " + field.type + ".");
@@ -932,7 +1042,7 @@
         InstructionListIterator iterator = block.listIterator();
         while (iterator.hasNext()) {
           Instruction current = iterator.next();
-          if (current.isStaticPut() && puts.values().contains(current.asStaticPut())) {
+          if (current.isStaticPut() && puts.contains(current.asStaticPut())) {
             iterator.remove();
             // Collect, for removal, the instruction that created the value for the static put,
             // if all users are gone. This is done even if these instructions can throw as for
@@ -1021,7 +1131,7 @@
         if (current.isInvoke() && current.asInvoke().requiredArgumentRegisters() > 5) {
           Invoke invoke = current.asInvoke();
           it.previous();
-          Map<ConstNumber, ConstNumber> oldToNew = new HashMap<>();
+          Map<ConstNumber, ConstNumber> oldToNew = new IdentityHashMap<>();
           for (int i = 0; i < invoke.inValues().size(); i++) {
             Value value = invoke.inValues().get(i);
             if (value.isConstNumber() && value.numberOfUsers() > 1) {
@@ -1101,18 +1211,18 @@
             // Add constant into the dominator block of usages.
             insertConstantInBlock(instruction, entry.getKey());
           } else {
-            assert instruction.outValue().numberOfUsers() != 0;
             ConstNumber constNumber = instruction.asConstNumber();
             Value constantValue = instruction.outValue();
-            List<Instruction> toRemove = new ArrayList<>(constantValue.uniqueUsers().size());
+            assert constantValue.numberOfUsers() != 0;
+            assert constantValue.numberOfUsers() == constantValue.numberOfAllUsers();
             for (Instruction user : constantValue.uniqueUsers()) {
               ConstNumber newCstNum = ConstNumber.copyOf(code, constNumber);
               InstructionListIterator iterator = user.getBlock().listIterator(user);
               iterator.previous();
               iterator.add(newCstNum);
-              user.replaceValue(constantValue, newCstNum.outValue(), toRemove);
+              user.replaceValue(constantValue, newCstNum.outValue());
             }
-            toRemove.forEach(constantValue::removeUser);
+            constantValue.clearUsers();
           }
         }
       } else {
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 b90d2ad..a11d924 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
@@ -23,7 +23,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
-import com.android.tools.r8.ir.conversion.CallGraph;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
@@ -36,6 +36,7 @@
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 public class Inliner {
@@ -136,7 +137,8 @@
           .sorted(DexEncodedMethod::slowCompare)
           .collect(Collectors.toList());
       for (DexEncodedMethod method : methods) {
-        converter.processMethod(method, feedback, Outliner::noProcessing);
+        converter.processMethod(method, feedback, x -> false, CallSiteInformation.empty(),
+            Outliner::noProcessing);
         assert method.isProcessed();
       }
     }
@@ -222,7 +224,7 @@
       this.reason = reason;
     }
 
-    boolean forceInline() {
+    boolean ignoreInstructionBudget() {
       return reason != Reason.SIMPLE;
     }
 
@@ -315,7 +317,9 @@
     return true;
   }
 
-  public void performInlining(DexEncodedMethod method, IRCode code, CallGraph callGraph)
+  public void performInlining(DexEncodedMethod method, IRCode code,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      CallSiteInformation callSiteInformation)
       throws ApiLevelException {
     int instruction_allowance = 1500;
     instruction_allowance -= numberOfInstructions(code);
@@ -323,7 +327,8 @@
       return;
     }
     computeReceiverMustBeNonNull(code);
-    InliningOracle oracle = new InliningOracle(this, method, callGraph);
+    InliningOracle oracle = new InliningOracle(this, method, callSiteInformation,
+        isProcessedConcurrently);
 
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
@@ -339,15 +344,7 @@
           InvokeMethod invoke = current.asInvokeMethod();
           InlineAction result = invoke.computeInlining(oracle);
           if (result != null) {
-            DexEncodedMethod target = appInfo.lookup(invoke.getType(), invoke.getInvokedMethod());
-            if (target == null) {
-              // The declared target cannot be found so skip inlining.
-              continue;
-            }
-            if (!(target.isProcessed() || result.reason == Reason.FORCE)) {
-              // Do not inline code that was not processed unless we have to force inline.
-              continue;
-            }
+            DexEncodedMethod target = result.target;
             IRCode inlinee = result
                 .buildIR(code.valueNumberGenerator, appInfo, graphLense, options);
             if (inlinee != null) {
@@ -355,10 +352,6 @@
               if (block.hasCatchHandlers() && inlinee.getNormalExitBlock() == null) {
                 continue;
               }
-              if (callGraph.isBreaker(method, target)) {
-                // Make sure we don't inline a call graph breaker.
-                continue;
-              }
               // If this code did not go through the full pipeline, apply inlining to make sure
               // that force inline targets get processed.
               if (!target.isProcessed()) {
@@ -366,7 +359,8 @@
                 if (Log.ENABLED) {
                   Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
                 }
-                performInlining(target, inlinee, callGraph);
+                performInlining(target, inlinee, isProcessedConcurrently,
+                    callSiteInformation);
               }
               // Make sure constructor inlining is legal.
               assert !target.isClassInitializer();
@@ -378,7 +372,7 @@
               if (invoke.isInvokeMethodWithReceiver()) {
                 // If the invoke has a receiver but the declared method holder is different
                 // from the computed target holder, inlining requires a downcast of the receiver.
-                if (result.target.method.getHolder() != target.method.getHolder()) {
+                if (target.method.getHolder() != invoke.getInvokedMethod().getHolder()) {
                   downcast = result.target.method.getHolder();
                 }
               }
@@ -386,7 +380,7 @@
               // Back up before the invoke instruction.
               iterator.previous();
               instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.forceInline()) {
+              if (instruction_allowance >= 0 || result.ignoreInstructionBudget()) {
                 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 91f0a61..8b7a05e 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
@@ -11,10 +11,11 @@
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokePolymorphic;
 import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.conversion.CallGraph;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
 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;
+import java.util.function.Predicate;
 
 /**
  * The InliningOracle contains information needed for when inlining
@@ -26,16 +27,19 @@
 
   private final Inliner inliner;
   private final DexEncodedMethod method;
-  private final CallGraph callGraph;
+  private final CallSiteInformation callSiteInformation;
+  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
   private final InliningInfo info;
 
   InliningOracle(
       Inliner inliner,
       DexEncodedMethod method,
-      CallGraph callGraph) {
+      CallSiteInformation callSiteInformation,
+      Predicate<DexEncodedMethod> isProcessedConcurrently) {
     this.inliner = inliner;
     this.method = method;
-    this.callGraph = callGraph;
+    this.callSiteInformation = callSiteInformation;
+    this.isProcessedConcurrently = isProcessedConcurrently;
     info = Log.ENABLED ? new InliningInfo(method) : null;
   }
 
@@ -66,7 +70,7 @@
         && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
       return Reason.ALWAYS;
     }
-    if (callGraph.hasSingleCallSite(target)) {
+    if (callSiteInformation.hasSingleCallSite(target)) {
       return Reason.SINGLE_CALLER;
     }
     if (isDoubleInliningTarget(target)) {
@@ -91,18 +95,13 @@
 
   private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
     // 10 is found from measuring.
-    return callGraph.hasDoubleCallSite(candidate)
+    return callSiteInformation.hasDoubleCallSite(candidate)
         && candidate.getCode().isDexCode()
         && (candidate.getCode().asDexCode().instructions.length <= 10);
   }
 
   private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
       Reason reason) {
-    if (callGraph.isBreaker(method, candidate)) {
-      // Cycle breaker so abort to preserve compilation order.
-      return false;
-    }
-
     if (method == candidate) {
       // Cannot handle recursive inlining at this point.
       // Force inlined method should never be recursive.
@@ -113,6 +112,13 @@
       return false;
     }
 
+    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "is processed in parallel");
+      }
+      return false;
+    }
+
     // Abort inlining attempt if method -> target access is not right.
     if (!inliner.hasInliningAccess(method, candidate)) {
       if (info != null) {
@@ -176,7 +182,7 @@
     }
 
     Reason reason = computeInliningReason(candidate);
-    if (!candidate.isInliningCandidate(method, reason == Reason.FORCE, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -202,7 +208,7 @@
 
     Reason reason = computeInliningReason(candidate);
     // Determine if this should be inlined no matter how big it is.
-    if (!candidate.isInliningCandidate(method, reason == Reason.FORCE, inliner.appInfo)) {
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
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 f6d9c26..ade7537 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
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.BasicBlock.ThrowingInfo;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.Div;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -929,6 +930,16 @@
     }
 
     @Override
+    public int getMoveExceptionRegister() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public DebugPosition getDebugPositionAtOffset(int offset) {
+      throw new Unreachable();
+    }
+
+    @Override
     public boolean verifyCurrentInstructionCanThrow() {
       // TODO(sgjesse): Check more here?
       return true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index a7d6e8d..35cadf2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -84,7 +84,9 @@
             commonSuffixSize = Math.min(
                 commonSuffixSize, sharedSuffixSizeExcludingExit(firstPred, pred, allocator));
           }
-          assert commonSuffixSize >= 1;
+          if (commonSuffixSize == 0) {
+            continue;
+          }
           int blockNumber = startNumberOfNewBlock + newBlocks.size();
           BasicBlock newBlock = createAndInsertBlockForSuffix(
               blockNumber, commonSuffixSize, predsWithSameLastInstruction, block);
@@ -160,7 +162,9 @@
       Instruction i0 = it0.previous();
       Instruction i1 = it1.previous();
       if (!i0.identicalAfterRegisterAllocation(i1, allocator)) {
-        return suffixSize;
+        // If the shared suffix follows a debug position at least one instruction must remain
+        // unshared to ensure the debug position is at a different pc than the shared suffix.
+        return i0.isDebugPosition() || i1.isDebugPosition() ? suffixSize - 1 : suffixSize;
       }
       suffixSize++;
     }
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 6cd453b..52653a7 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
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.Add;
 import com.android.tools.r8.ir.code.And;
+import com.android.tools.r8.ir.code.ArithmeticBinop;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.IRCode;
@@ -24,7 +25,6 @@
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.ir.regalloc.RegisterPositions.Type;
 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;
@@ -39,7 +39,6 @@
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -139,8 +138,7 @@
   // List of intervals where the current instruction falls into one of their live range holes.
   private List<LiveIntervals> inactive = new LinkedList<>();
   // List of intervals that no register has been allocated to sorted by first live range.
-  private PriorityQueue<LiveIntervals> unhandled =
-      new PriorityQueue<>(Comparator.comparingInt(LiveIntervals::getStart));
+  private PriorityQueue<LiveIntervals> unhandled = new PriorityQueue<>();
 
   // The first register used for parallel moves. After register allocation the parallel move
   // temporary registers are [firstParallelMoveTemporary, maxRegisterNumber].
@@ -197,7 +195,6 @@
       }
     }
 
-    clearUserInfo();
     assert code.isConsistentGraph();
     if (Log.ENABLED) {
       Log.debug(this.getClass(), toString());
@@ -206,6 +203,7 @@
     if (debug) {
       computeDebugInfo(blocks);
     }
+    clearUserInfo();
     clearState();
   }
 
@@ -814,6 +812,9 @@
     // Go through each unhandled live interval and find a register for it.
     while (!unhandled.isEmpty()) {
       LiveIntervals unhandledInterval = unhandled.poll();
+
+      setHintToPromote2AddrInstruction(unhandledInterval);
+
       // If this interval value is the src of an argument move. Fix the registers for the
       // consecutive arguments now and add hints to the move sources. This looks forward
       // and propagate hints backwards to avoid many moves in connection with ranged invokes.
@@ -865,6 +866,31 @@
     return true;
   }
 
+  /*
+   * This method tries to promote arithmetic binary instruction to use the 2Addr form.
+   * To achieve this goal the output interval of the binary instruction is set with an hint
+   * that is the left interval or the right interval if possible when intervals do not overlap.
+   */
+  private void setHintToPromote2AddrInstruction(LiveIntervals unhandledInterval) {
+    if (unhandledInterval.getHint() == null &&
+        unhandledInterval.getValue().definition instanceof ArithmeticBinop) {
+      ArithmeticBinop binOp = unhandledInterval.getValue().definition.asArithmeticBinop();
+      Value left = binOp.leftValue();
+      assert left != null;
+      if (left.getLiveIntervals() != null &&
+          !left.getLiveIntervals().overlaps(unhandledInterval)) {
+        unhandledInterval.setHint(left.getLiveIntervals());
+      } else {
+        Value right = binOp.rightValue();
+        assert right != null;
+        if (binOp.isCommutative() && right.getLiveIntervals() != null &&
+            !right.getLiveIntervals().overlaps(unhandledInterval)) {
+          unhandledInterval.setHint(right.getLiveIntervals());
+        }
+      }
+    }
+  }
+
   /**
    * Perform look-ahead and allocate registers for linked argument chains that have the argument
    * interval as an argument move source.
@@ -2216,25 +2242,6 @@
     return true;
   }
 
-  public void print(CfgPrinter printer, String title) {
-    printer.begin("intervals");
-    printer.print("name \"").append(title).append("\"").ln();
-    PriorityQueue<LiveIntervals> sortedIntervals =
-        new PriorityQueue<>((o1, o2) -> Integer.compare(o1.getStart(), o2.getStart()));
-    sortedIntervals.addAll(liveIntervals);
-    for (LiveIntervals interval = sortedIntervals.poll();
-        interval != null;
-        interval = sortedIntervals.poll()) {
-      Value value = interval.getValue();
-      if (interval.getRanges().get(0).isInfinite()) {
-        // Skip argument sentinels.
-        continue;
-      }
-      interval.print(printer, value.getNumber(), value.getNumber());
-    }
-    printer.end("intervals");
-  }
-
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder("Live ranges:\n");
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 65628c3..0d8c6b8 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
@@ -15,7 +15,7 @@
 import java.util.List;
 import java.util.TreeSet;
 
-public class LiveIntervals {
+public class LiveIntervals implements Comparable<LiveIntervals> {
 
   private final Value value;
   private LiveIntervals nextConsecutive;
@@ -454,6 +454,12 @@
   }
 
   @Override
+  public int compareTo(LiveIntervals other) {
+    int startDiff = getStart() - other.getStart();
+    return startDiff != 0 ? startDiff : (value.getNumber() - other.value.getNumber());
+  }
+
+  @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
     builder.append("(cons ");
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
index 9cb873e..4a21e8a 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
@@ -10,7 +10,7 @@
 
 // Register moves used by the spilling register allocator. These are used both for spill and
 // for phi moves and they are moves between actual registers represented by their register number.
-public class RegisterMove {
+public class RegisterMove implements Comparable<RegisterMove> {
   MoveType type;
   int dst;
   int src;
@@ -70,4 +70,30 @@
     RegisterMove o = (RegisterMove) other;
     return o.src == src && o.dst == dst && o.type == type && o.definition == definition;
   }
+
+  @Override
+  public int compareTo(RegisterMove o) {
+    int srcDiff = src - o.src;
+    if (srcDiff != 0) {
+      return srcDiff;
+    }
+    int dstDiff = dst - o.dst;
+    if (dstDiff != 0) {
+      return dstDiff;
+    }
+    int typeDiff = o.type.ordinal() - type.ordinal();
+    if (typeDiff != 0) {
+      return typeDiff;
+    }
+    if (definition == null) {
+      if (o.definition != null) {
+        return -1;
+      }
+      return 0;
+    }
+    if (o.definition == null) {
+      return 1;
+    }
+    return definition.getNumber() - o.definition.getNumber();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
index cc26729..007aa6f 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
@@ -14,15 +14,15 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
 public class RegisterMoveScheduler {
   // The set of moves to schedule.
-  private Set<RegisterMove> moveSet = new LinkedHashSet<>();
+  private Set<RegisterMove> moveSet = new TreeSet<>();
   // Mapping to keep track of which values currently corresponds to each other.
   // This is initially an identity map but changes as we insert moves.
   private Map<Integer, Integer> valueMap = new HashMap<>();
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 5fa247e..391b479 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
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.MoveType;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -198,6 +199,16 @@
   }
 
   @Override
+  public int getMoveExceptionRegister() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public DebugPosition getDebugPositionAtOffset(int offset) {
+    throw new Unreachable();
+  }
+
+  @Override
   public final boolean verifyCurrentInstructionCanThrow() {
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/naming/DictionaryReader.java b/src/main/java/com/android/tools/r8/naming/DictionaryReader.java
index 146d531..1d05d25 100644
--- a/src/main/java/com/android/tools/r8/naming/DictionaryReader.java
+++ b/src/main/java/com/android/tools/r8/naming/DictionaryReader.java
@@ -9,7 +9,6 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.List;
 
 public class DictionaryReader implements AutoCloseable {
 
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index a26e135..6a58866 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 564e6de..0b683e2 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -64,13 +64,14 @@
 
   private final BufferedReader reader;
 
+  @Override
   public void close() throws IOException {
     if (reader != null) {
       reader.close();
     }
   }
 
-  private ProguardMapReader(BufferedReader reader) throws IOException {
+  private ProguardMapReader(BufferedReader reader) {
     this.reader = reader;
   }
 
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 5292a45..22fdf45 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -22,11 +22,8 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.CharBuffer;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -85,31 +82,32 @@
   }
 
   public void parse(Path path) throws ProguardRuleParserException, IOException {
-    parse(Collections.singletonList(path));
+    parse(ImmutableList.of(new ProguardConfigurationSourceFile(path)));
   }
 
-  public void parse(List<Path> pathList) throws ProguardRuleParserException, IOException {
-    for (Path path : pathList) {
-      new ProguardFileParser(path).parse();
+  public void parse(ProguardConfigurationSource source)
+      throws ProguardRuleParserException, IOException {
+    parse(ImmutableList.of(source));
+  }
+
+  public void parse(List<ProguardConfigurationSource> sources)
+      throws ProguardRuleParserException, IOException {
+    for (ProguardConfigurationSource source : sources) {
+      new ProguardFileParser(source).parse();
     }
   }
 
   private class ProguardFileParser {
 
-    private final Path path;
+    private final String name;
     private final String contents;
     private int position = 0;
     private Path baseDirectory;
 
-    ProguardFileParser(Path path) throws IOException {
-      this.path = path;
-      contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
-      baseDirectory = path.getParent();
-      if (baseDirectory == null) {
-        // path parent can be null only if it's root dir or if its a one element path relative to
-        // current directory.
-        baseDirectory = Paths.get(".");
-      }
+    ProguardFileParser(ProguardConfigurationSource source) throws IOException {
+      contents = source.get();
+      baseDirectory = source.getBaseDirectory();
+      name = source.getName();
     }
 
     public void parse() throws ProguardRuleParserException {
@@ -271,7 +269,7 @@
     private void parseInclude() throws ProguardRuleParserException {
       Path included = parseFileName();
       try {
-        new ProguardFileParser(included).parse();
+        new ProguardFileParser(new ProguardConfigurationSourceFile(included)).parse();
       } catch (FileNotFoundException | NoSuchFileException e) {
         throw parseError("Included file '" + included.toString() + "' not found", e);
       } catch (IOException e) {
@@ -1037,12 +1035,12 @@
         String line = lines[lineNumber];
         if (remaining <= line.length() || lineNumber == lines.length - 1) {
           String arrow = CharBuffer.allocate(remaining).toString().replace( '\0', ' ' ) + '^';
-          return path.toString() + ":" + (lineNumber + 1) + ":" + remaining + "\n" + line
+          return name + ":" + (lineNumber + 1) + ":" + remaining + "\n" + line
               + '\n' + arrow;
         }
         remaining -= (line.length() + 1); // Include newline.
       }
-      return path.toString();
+      return name;
     }
 
     private ProguardRuleParserException parseError(String message) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java
new file mode 100644
index 0000000..b0375c0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSource.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public interface ProguardConfigurationSource {
+  String get() throws IOException;
+  Path getBaseDirectory();
+  String getName();
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java
new file mode 100644
index 0000000..058c14f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceFile.java
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ProguardConfigurationSourceFile implements ProguardConfigurationSource {
+  private final Path path;
+
+  public ProguardConfigurationSourceFile(Path path) {
+    this.path = path;
+  }
+
+  public String get() throws IOException{
+    return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
+  }
+
+  public Path getBaseDirectory() {
+    Path baseDirectory = path.getParent();
+    if (baseDirectory == null) {
+      // Path parent can be null only if it's root dir or if its a one element path relative to
+      // current directory.
+      baseDirectory = Paths.get(".");
+    }
+    return baseDirectory;
+  }
+
+  public String getName() {
+    return path.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
new file mode 100644
index 0000000..ac5f0ab
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.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.shaking;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import joptsimple.internal.Strings;
+
+public class ProguardConfigurationSourceStrings implements ProguardConfigurationSource {
+  private final List<String> config;
+
+  public ProguardConfigurationSourceStrings(List<String> config) {
+    this.config = config;
+  }
+
+  public String get() throws IOException{
+    return Strings.join(config, "\n");
+  }
+
+  public Path getBaseDirectory() {
+    return Paths.get(".");
+  }
+
+  public String getName() {
+    return "<no file>";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 5e7b42d..4dd1880 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -28,6 +28,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -572,5 +573,29 @@
       return Collections
           .unmodifiableMap(dependentNoShrinking.getOrDefault(item, Collections.emptyMap()));
     }
+
+    @Override
+    public String toString() {
+      StringBuilder builder = new StringBuilder();
+      builder.append("RootSet");
+
+      builder.append("\nnoShrinking: " + noShrinking.size());
+      builder.append("\nnoOptimization: " + noOptimization.size());
+      builder.append("\nnoObfuscation: " + noObfuscation.size());
+      builder.append("\nreasonAsked: " + reasonAsked.size());
+      builder.append("\nkeepPackageName: " + keepPackageName.size());
+      builder.append("\ncheckDiscarded: " + checkDiscarded.size());
+      builder.append("\nnoSideEffects: " + noSideEffects.size());
+      builder.append("\nassumedValues: " + assumedValues.size());
+      builder.append("\ndependentNoShrinking: " + dependentNoShrinking.size());
+
+      builder.append("\n\nNo Shrinking:");
+      noShrinking.keySet().stream()
+          .sorted(Comparator.comparing(DexItem::toSourceString))
+          .forEach(a -> builder
+              .append("\n").append(a.toSourceString()).append(" ").append(noShrinking.get(a)));
+      builder.append("\n");
+      return builder.toString();
+    }
   }
 }
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 153f604..3decec1 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -39,7 +38,7 @@
         ? new UsagePrinter() : UsagePrinter.DONT_PRINT;
   }
 
-  public DexApplication run() throws IOException {
+  public DexApplication run() {
     application.timing.begin("Pruning application...");
     if (options.debugKeepRules && !options.skipMinification) {
       System.out.println(
diff --git a/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java
index 4610f41..09bbad6 100644
--- a/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/protolite/ProtoLitePruner.java
@@ -137,8 +137,7 @@
   }
 
   private boolean isDefinedAsNull(Value value) {
-    return value.definition != null && value.definition.isConstNumber()
-        && value.definition.asConstNumber().isZero();
+    return value.definition != null && value.isZero();
   }
 
   private boolean isComputeSizeMethod(DexMethod invokedMethod) {
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
new file mode 100644
index 0000000..d72fb2b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -0,0 +1,50 @@
+// 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;
+
+/**
+ * Android API level description
+ */
+public enum AndroidApiLevel {
+  O(26),
+  N_MR1(25),
+  N(24),
+  M(23),
+  L_MR1(22),
+  L(21),
+  K_WATCH(20),
+  K(19),
+  J_MR2(18),
+  J_MR1(17),
+  J(16),
+  I_MR1(15),
+  I(14),
+  H_MR2(13),
+  H_MR1(12),
+  H(11),
+  G_MR1(10),
+  G(9),
+  F(8),
+  E_MR1(7),
+  E_0_1(6),
+  E(5),
+  D(4),
+  C(3),
+  B_1_1(2),
+  B(1);
+
+  private final int level;
+
+  AndroidApiLevel(int level) {
+    this.level = level;
+  }
+
+  public int getLevel() {
+    return level;
+  }
+
+  public String getName() {
+    return "Android " + name();
+  }
+}
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 4c6c56f..a4c2c62 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -32,6 +32,7 @@
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -302,9 +303,11 @@
    */
   public void writeToDirectory(Path directory, OutputMode outputMode) throws IOException {
     if (outputMode == OutputMode.Indexed) {
-      for (Path path : Files.list(directory).collect(Collectors.toList())) {
-        if (isClassesDexFile(path)) {
-          Files.delete(path);
+      try (Stream<Path> filesInDir = Files.list(directory)) {
+        for (Path path : filesInDir.collect(Collectors.toList())) {
+          if (isClassesDexFile(path)) {
+            Files.delete(path);
+          }
         }
       }
     }
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 9f3488c..c241bd7 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -14,6 +14,7 @@
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 
 public class InternalOptions {
@@ -197,7 +198,7 @@
 
   public static class TestingOptions {
 
-    public Function<List<DexEncodedMethod>, List<DexEncodedMethod>> irOrdering =
+    public Function<Set<DexEncodedMethod>, Set<DexEncodedMethod>> irOrdering =
         Function.identity();
   }
 
@@ -321,37 +322,37 @@
   }
 
   public boolean canUseInvokePolymorphic() {
-    return minApiLevel >= Constants.ANDROID_O_API;
+    return minApiLevel >= AndroidApiLevel.O.getLevel();
   }
 
   public boolean canUseInvokeCustom() {
-    return minApiLevel >= Constants.ANDROID_O_API;
+    return minApiLevel >= AndroidApiLevel.O.getLevel();
   }
 
   public boolean canUseDefaultAndStaticInterfaceMethods() {
-    return minApiLevel >= Constants.ANDROID_N_API;
+    return minApiLevel >= AndroidApiLevel.N.getLevel();
   }
 
   public boolean canUsePrivateInterfaceMethods() {
-    return minApiLevel >= Constants.ANDROID_N_API;
+    return minApiLevel >= AndroidApiLevel.N.getLevel();
   }
 
   public boolean canUseMultidex() {
-    return intermediate || minApiLevel >= Constants.ANDROID_L_API;
+    return intermediate || minApiLevel >= AndroidApiLevel.L.getLevel();
   }
 
   public boolean canUseLongCompareAndObjectsNonNull() {
-    return minApiLevel >= Constants.ANDROID_K_API;
+    return minApiLevel >= AndroidApiLevel.K.getLevel();
   }
 
   public boolean canUseSuppressedExceptions() {
-    return minApiLevel >= Constants.ANDROID_K_API;
+    return minApiLevel >= AndroidApiLevel.K.getLevel();
   }
 
   // APIs for accessing parameter names annotations are not available before Android O, thus does
   // not emit them to avoid wasting space in Dex files because runtimes before Android O will ignore
   // them.
   public boolean canUseParameterNameAnnotations() {
-    return minApiLevel >= Constants.ANDROID_O_API;
+    return minApiLevel >= AndroidApiLevel.O.getLevel();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index 07c136d..5ea2a30 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -16,15 +16,16 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.Enumeration;
 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;
+import java.util.zip.ZipFile;
 
 /** Lazy Java class file resource provider based on preloaded/prebuilt context. */
 public final class PreloadedClassFileProvider implements ClassFileResourceProvider {
@@ -52,17 +53,17 @@
   public static ClassFileResourceProvider fromArchive(Path archive) throws IOException {
     assert isArchive(archive);
     Builder builder = builder();
-    try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
-      ZipEntry entry;
-      while ((entry = stream.getNextEntry()) != null) {
+    try (ZipFile zipFile = new ZipFile(archive.toFile())) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
         String name = entry.getName();
         if (isClassFile(Paths.get(name))) {
-          builder.addResource(guessTypeDescriptor(name), ByteStreams.toByteArray(stream));
+          try (InputStream entryStream = zipFile.getInputStream(entry)) {
+            builder.addResource(guessTypeDescriptor(name), ByteStreams.toByteArray(entryStream));
+          }
         }
       }
-    } catch (ZipException e) {
-      throw new CompilationError(
-          "Zip error while reading '" + archive + "': " + e.getMessage(), e);
     }
 
     return builder.build();
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramFileArchiveReader.java b/src/main/java/com/android/tools/r8/utils/ProgramFileArchiveReader.java
index 8341921..79ab853 100644
--- a/src/main/java/com/android/tools/r8/utils/ProgramFileArchiveReader.java
+++ b/src/main/java/com/android/tools/r8/utils/ProgramFileArchiveReader.java
@@ -10,17 +10,18 @@
 import com.android.tools.r8.Resource;
 import com.android.tools.r8.errors.CompilationError;
 import com.google.common.io.ByteStreams;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
-import java.util.zip.ZipInputStream;
+import java.util.zip.ZipFile;
 
 class ProgramFileArchiveReader {
 
@@ -38,21 +39,24 @@
     assert isArchive(archive);
     dexResources = new ArrayList<>();
     classResources = new ArrayList<>();
-    try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
-      ZipEntry entry;
-      while ((entry = stream.getNextEntry()) != null) {
-        Path name = Paths.get(entry.getName());
-        if (isDexFile(name)) {
-          if (!ignoreDexInArchive) {
-            Resource resource =
-                new OneShotByteResource(Resource.Kind.DEX, ByteStreams.toByteArray(stream), null);
-            dexResources.add(resource);
+    try (ZipFile zipFile = new ZipFile(archive.toFile())) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        try (InputStream stream = zipFile.getInputStream(entry)) {
+          Path name = Paths.get(entry.getName());
+          if (isDexFile(name)) {
+            if (!ignoreDexInArchive) {
+              Resource resource =
+                  new OneShotByteResource(Resource.Kind.DEX, ByteStreams.toByteArray(stream), null);
+              dexResources.add(resource);
+            }
+          } else if (isClassFile(name)) {
+            String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
+            Resource resource = new OneShotByteResource(Resource.Kind.CLASSFILE,
+                ByteStreams.toByteArray(stream), Collections.singleton(descriptor));
+            classResources.add(resource);
           }
-        } else if (isClassFile(name)) {
-          String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
-          Resource resource = new OneShotByteResource(Resource.Kind.CLASSFILE,
-              ByteStreams.toByteArray(stream), Collections.singleton(descriptor));
-          classResources.add(resource);
         }
       }
     } catch (ZipException e) {
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 47d8e01..ca9a404 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -6,27 +6,31 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.google.common.io.ByteStreams;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
+import java.util.zip.ZipFile;
 
 public class ZipUtils {
 
   public interface OnEntryHandler {
-    void onEntry(ZipEntry entry, ZipInputStream input) throws IOException;
+    void onEntry(ZipEntry entry, InputStream input) throws IOException;
   }
 
-  public static void iter(String zipFile, OnEntryHandler handler) throws IOException {
-    try (ZipInputStream input = new ZipInputStream(new FileInputStream(zipFile))){
-      ZipEntry entry;
-      while ((entry = input.getNextEntry()) != null) {
-        handler.onEntry(entry, input);
+  public static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFileStr)) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
+        try (InputStream entryStream = zipFile.getInputStream(entry)) {
+          handler.onEntry(entry, entryStream);
+        }
       }
     }
   }
diff --git a/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
new file mode 100644
index 0000000..fa6078a
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerAssignmentInitialization.java
@@ -0,0 +1,16 @@
+// 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.
+
+public class ClassInitializerAssignmentInitialization {
+
+  static int x = 1;
+  static int y;
+
+  static int z = 2;
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerEmpty.java b/src/test/debugTestResources/ClassInitializerEmpty.java
new file mode 100644
index 0000000..9062d62
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerEmpty.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.
+
+public class ClassInitializerEmpty {
+
+  static {
+  }
+
+  public static void main(String[] args) {
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerMixedInitialization.java b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
new file mode 100644
index 0000000..504db6c
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerMixedInitialization.java
@@ -0,0 +1,25 @@
+// 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.
+
+public class ClassInitializerMixedInitialization {
+
+  static boolean b;
+  static int x = 1;
+  static int y;
+
+  static {
+    x = 2;
+    if (b) {
+      y = 1;
+    } else {
+      y = 2;
+    }
+    x = 3;
+  }
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
new file mode 100644
index 0000000..78fe9a0
--- /dev/null
+++ b/src/test/debugTestResources/ClassInitializerStaticBlockInitialization.java
@@ -0,0 +1,26 @@
+// 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.
+
+public class ClassInitializerStaticBlockInitialization {
+
+  static boolean b;
+  static int x;
+  static int y;
+
+  static {
+    x = 1;
+    x = 2;
+    if (b) {
+      y = 1;
+    } else {
+      y = 2;
+    }
+    x = 3;
+  }
+
+  public static void main(String[] args) {
+    System.out.println("x=" + x);
+    System.out.println("y=" + y);
+  }
+}
diff --git a/src/test/debugTestResources/Locals.java b/src/test/debugTestResources/Locals.java
index d42dd61..5afcabb 100644
--- a/src/test/debugTestResources/Locals.java
+++ b/src/test/debugTestResources/Locals.java
@@ -244,6 +244,80 @@
     return sum + x + y;
   }
 
+  public static int argumentLiveAtReturn(int x) {
+    switch (x) {
+      case 0:
+        return 0;
+      case 1:
+        return 0;
+      case 2:
+        return 0;
+      case 100:
+        return 1;
+      case 101:
+        return 1;
+      case 102:
+        return 1;
+    }
+    return -1;
+  }
+
+  public static int switchRewriteToIfs(int x) {
+    {
+      int t = x + 1;
+      x = t;
+      x = x + x;
+    }
+    switch (x) {
+      case 0:
+        return 0;
+      case 100:
+        return 1;
+    }
+    return -1;
+  }
+
+  public static int switchRewriteToSwitches(int x) {
+    {
+      int t = x + 1;
+      x = t;
+      x = x + x;
+    }
+    switch (x) {
+      case 0:
+        return 0;
+      case 1:
+        return 0;
+      case 2:
+        return 0;
+      case 100:
+        return 1;
+      case 101:
+        return 1;
+      case 102:
+        return 1;
+    }
+    return -1;
+  }
+
+  public static String regression65039701(boolean createIntNotLong) {
+    Object a = createIntNotLong ? new int[1] : new long[1];
+    if (a instanceof int []) {
+      ((int [])a)[0] = 0;
+    }
+    return "OK";
+  }
+
+  public static void regression65066975(boolean bit) {
+    nop();
+    if (bit) {
+      nop();
+    } else {
+      nop();
+    }
+    nop();
+  }
+
   public static void main(String[] args) {
     noLocals();
     unusedLocals();
@@ -259,5 +333,10 @@
     stepNonEmptyForLoopBody(3);
     tempInCase(42);
     localSwap(1, 2);
+    argumentLiveAtReturn(-1);
+    switchRewriteToIfs(1);
+    switchRewriteToSwitches(1);
+    regression65039701(true);
+    regression65066975(false);
   }
 }
diff --git a/src/test/debugTestResourcesKotlin/KotlinApp.kt b/src/test/debugTestResourcesKotlin/KotlinApp.kt
index 7fa2be1..094407b 100644
--- a/src/test/debugTestResourcesKotlin/KotlinApp.kt
+++ b/src/test/debugTestResourcesKotlin/KotlinApp.kt
@@ -3,42 +3,27 @@
 // BSD-style license that can be found in the LICENSE file.
 
 class KotlinApp {
+
+    fun ifElse(cond: Boolean) {
+        val a = 10
+        if (cond) {
+            val b = a * 2
+            printInt(b)
+        } else {
+            val c = a / 2
+            print(c)
+        }
+    }
+
+    fun printInt(i: Int) {
+        println(i)
+    }
+
     companion object {
         @JvmStatic fun main(args: Array<String>) {
-            println("Hello world!")
             val instance = KotlinApp()
-            instance.processObject(instance, instance::printObject)
-            instance.invokeInlinedFunctions()
+            instance.ifElse(true)
+            instance.ifElse(false)
         }
     }
-
-    fun processObject(obj: Any, func: (Any) -> Unit) {
-        func(obj)
-    }
-
-    fun printObject(obj: Any) {
-        println(obj)
-    }
-
-    fun invokeInlinedFunctions() {
-        inlinedA {
-            val inA = 1
-            inlinedB {
-                val inB = 2
-                foo(inA, inB)
-            }
-        }
-    }
-
-    inline fun inlinedA(f: () -> Unit) {
-        f()
-    }
-
-    inline fun inlinedB(f: () -> Unit) {
-        f()
-    }
-
-    fun foo(a: Int, b: Int) {
-        println("a=$a, b=$b")
-    }
 }
\ No newline at end of file
diff --git a/src/test/debugTestResourcesKotlin/KotlinInline.kt b/src/test/debugTestResourcesKotlin/KotlinInline.kt
new file mode 100644
index 0000000..7f914e4
--- /dev/null
+++ b/src/test/debugTestResourcesKotlin/KotlinInline.kt
@@ -0,0 +1,59 @@
+// 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.
+
+class KotlinInline {
+
+    fun processObject(obj: Any, func: (Any) -> Unit) {
+        func(obj)
+    }
+
+    fun printObject(obj: Any) {
+        println(obj)
+    }
+
+    fun invokeInlinedFunctions() {
+        inlinedA {
+            val inA = 1
+            inlinedB {
+                val inB = 2
+                foo(inA, inB)
+            }
+        }
+    }
+
+    inline fun inlinedA(f: () -> Unit) {
+        f()
+    }
+
+    inline fun inlinedB(f: () -> Unit) {
+        f()
+    }
+
+    fun foo(a: Int, b: Int) {
+        println("a=$a, b=$b")
+    }
+
+    fun emptyMethod(unused: Int) {
+    }
+
+    fun singleInline() {
+        emptyMethod(0)
+        inlined()
+        emptyMethod(1)
+    }
+
+    inline fun inlined() {
+        emptyMethod(-1)
+    }
+
+    companion object {
+        @JvmStatic fun main(args: Array<String>) {
+            println("Hello world!")
+            val instance = KotlinInline()
+            instance.processObject(instance, instance::printObject)
+            instance.invokeInlinedFunctions()
+            instance.singleInline()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index aa30615..8ffab3c 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -3,11 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import static com.android.tools.r8.TestCondition.compilers;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.JctfTestSpecifications.Outcome;
+import com.android.tools.r8.TestCondition.RuntimeSet;
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -82,6 +82,12 @@
   private static final String ART_TESTS_NATIVE_LIBRARY_DIR = "tests/2017-07-27/art/lib64";
   private static final String ART_LEGACY_TESTS_NATIVE_LIBRARY_DIR = "tests/2016-12-19/art/lib64";
 
+  private static final RuntimeSet LEGACY_RUNTIME = TestCondition.runtimes(
+      DexVm.ART_4_4_4,
+      DexVm.ART_5_1_1,
+      DexVm.ART_6_0_1,
+      DexVm.ART_7_0_0);
+
   // Input jar for jctf tests.
   private static final String JCTF_COMMON_JAR = "build/libs/jctfCommon.jar";
 
@@ -527,8 +533,6 @@
           // keeps the instances alive is dead and could be garbage collected. The compiler reuses
           // the register for the list and therefore there are no live instances.
           .put("099-vmdebug", TestCondition.any())
-          // This test relies on output on stderr, which we currently do not collect.
-          .put("143-string-value", TestCondition.any())
           .put(
               "800-smali",
               TestCondition.match(
@@ -577,7 +581,7 @@
               "370-dex-v37",
               TestCondition.match(
                   TestCondition.tools(DexTool.JACK, DexTool.DX),
-                  compilers(CompilerUnderTest.R8, CompilerUnderTest.D8),
+                  TestCondition.compilers(CompilerUnderTest.R8, CompilerUnderTest.D8),
                   TestCondition.runtimes(DexVm.ART_4_4_4, DexVm.ART_5_1_1, DexVm.ART_6_0_1)))
           // Array index out of bounds exception.
           .put("449-checker-bce", TestCondition.any())
@@ -681,21 +685,15 @@
           // iput on a static field.
           .put("600-verifier-fails", TestCondition.match(TestCondition.R8_COMPILER))
           // Contains a method that falls off the end without a return.
-          .put("606-erroneous-class", TestCondition
-              .match(TestCondition.tools(DexTool.DX, DexTool.JACK),
-                  TestCondition.R8_NOT_AFTER_D8_COMPILER))
-          .put("064-field-access", TestCondition
-              .match(TestCondition.tools(DexTool.NONE), TestCondition.D8_COMPILER))
+          .put("606-erroneous-class", TestCondition.match(
+              TestCondition.tools(DexTool.DX, DexTool.JACK),
+              TestCondition.R8_NOT_AFTER_D8_COMPILER,
+              LEGACY_RUNTIME))
           .build();
 
   // Tests that are invalid dex files and on which R8/D8 fails and that is OK.
   private static final Multimap<String, TestCondition> expectedToFailWithCompiler =
       new ImmutableListMultimap.Builder<String, TestCondition>()
-          // When starting from the Dex frontend we see two definitions of the same class coming
-          // from two differents dex files.
-          .put("064-field-access",
-              TestCondition.match(TestCondition.tools(DexTool.DX, DexTool.JACK),
-                  TestCondition.runtimes(DexVm.ART_DEFAULT)))
           // When starting from the Jar frontend we see the A$B class both from the Java source
           // code and from the smali dex code. We reject that because there are then two definitions
           // of the same class in the application. When running from the final dex files there is
@@ -703,18 +701,8 @@
           .put("121-modifiers", TestCondition.match(TestCondition.tools(DexTool.NONE)))
           // This test uses register r1 in method that is declared to only use 1 register (r0).
           .put("142-classloader2", TestCondition.match(TestCondition.R8_COMPILER))
-          // When starting from the Dex frontend we see two definitions of the same class coming
-          // from two differents dex files.
-          .put("162-method-resolution",
-              TestCondition.match(TestCondition.tools(DexTool.DX, DexTool.JACK)))
           // This test uses an uninitialized register.
           .put("471-uninitialized-locals", TestCondition.match(TestCondition.R8_COMPILER))
-          // When starting from the Dex frontend we see two definitions of the same class coming
-          // from two differents dex files.
-          .put("606-erroneous-class",
-              TestCondition
-                  .match(TestCondition.tools(DexTool.DX, DexTool.JACK), TestCondition.D8_COMPILER,
-                      TestCondition.runtimes(DexVm.ART_DEFAULT)))
           // This test is starting from invalid dex code. It splits up a double value and uses
           // the first register of a double with the second register of another double.
           .put("800-smali", TestCondition.match(TestCondition.R8_COMPILER))
@@ -730,9 +718,7 @@
 
   // Tests that does not have valid input for us to be compatible with jack/dx running.
   private static List<String> noInputJar = ImmutableList.of(
-      "064-field-access", // Missing classes2 dir (has src2)
       "097-duplicate-method", // No input class files.
-      "162-method-resolution", // Based on multiple jasmin files
       "630-safecast-array", // No input class files.
       "801-VoidCheckCast", // No input class files.
       "804-class-extends-itself", // No input class files.
@@ -768,12 +754,17 @@
   // Tests to skip on some conditions
   private static final Multimap<String, TestCondition> testToSkip =
       new ImmutableListMultimap.Builder<String, TestCondition>()
+          // When running R8 on dex input (D8, DX or JACK) this test non-deterministically fails
+          // with a compiler exception, due to invoke-virtual on an interface, or it completes but
+          // the output when run on Art is not as expected. b/65233869
+          .put("162-method-resolution",
+              TestCondition.match(
+                  TestCondition.tools(DexTool.DX, DexTool.JACK), TestCondition.R8_COMPILER))
           // Old runtimes used the legacy test directory which does not contain input for tools
           // NONE and DX.
           .put("952-invoke-custom", TestCondition.match(
               TestCondition.tools(DexTool.NONE, DexTool.DX),
-              TestCondition.runtimes(
-                  DexVm.ART_4_4_4, DexVm.ART_5_1_1, DexVm.ART_6_0_1, DexVm.ART_7_0_0)))
+              LEGACY_RUNTIME))
           // No input dex files for DX
           .put("952-invoke-custom-kinds", TestCondition.match(TestCondition.tools(DexTool.DX)))
           .build();
@@ -955,7 +946,7 @@
   private static Map<SpecificationKey, TestSpecification> getTestsMap(
       CompilerUnderTest compilerUnderTest, CompilationMode compilationMode, DexVm dexVm) {
     File artTestDir = new File(ART_TESTS_DIR);
-    if (dexVm != DexVm.ART_DEFAULT) {
+    if (LEGACY_RUNTIME.set.contains(dexVm)) {
       artTestDir = new File(ART_LEGACY_TESTS_DIR);
     }
     if (!artTestDir.exists()) {
@@ -1446,25 +1437,20 @@
 
     File[] inputFiles;
     if (toolchain == DexTool.NONE) {
-      File classes = new File(specification.directory, "classes");
-      inputFiles =
-          com.google.common.io.Files.fileTreeTraverser().breadthFirstTraversal(classes).filter(
-              (File f) -> !f.isDirectory()).toArray(File.class);
+      inputFiles = addFileTree(new File[0], new File(specification.directory, "classes"));
+      inputFiles = addFileTree(inputFiles, new File(specification.directory, "jasmin_classes"));
       File smali = new File(specification.directory, "smali");
       if (smali.exists()) {
         File smaliDex = new File(smali, "out.dex");
         assert smaliDex.exists();
         inputFiles = ObjectArrays.concat(inputFiles, smaliDex);
       }
-      File classes2 = new File(specification.directory, "classes2");
-      if (classes2.exists()) {
-        inputFiles = ObjectArrays.concat(inputFiles,
-            com.google.common.io.Files.fileTreeTraverser().breadthFirstTraversal(classes2).filter(
-                (File f) -> !f.isDirectory()).toArray(File.class), File.class);
-      }
+      inputFiles = addFileTree(inputFiles, new File(specification.directory, "classes2"));
+      inputFiles = addFileTree(inputFiles, new File(specification.directory, "jasmin_classes2"));
     } else {
       inputFiles =
-          specification.directory.listFiles((File file) -> file.getName().endsWith(".dex"));
+          specification.directory.listFiles((File file) ->
+              file.getName().endsWith(".dex") && !file.getName().startsWith("jasmin"));
     }
     List<String> fileNames = new ArrayList<>();
     for (File file : inputFiles) {
@@ -1503,6 +1489,18 @@
     }
   }
 
+  private File[] addFileTree(File[] files, File directory) {
+    if (!directory.exists()) {
+      return files;
+    }
+    return ObjectArrays.concat(
+        files,
+        com.google.common.io.Files.fileTreeTraverser().breadthFirstTraversal(directory)
+            .filter(f -> !f.isDirectory())
+            .toArray(File.class),
+        File.class);
+  }
+
   private void runArtTestDoRunOnArt(
       DexVm version,
       CompilerUnderTest compilerUnderTest,
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index cb8e307..4f84303 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -93,6 +93,10 @@
       return compareTo(other) > 0;
     }
 
+    public boolean isOlderThanOrEqual(DexVm other) {
+      return compareTo(other) <= 0;
+    }
+
     private DexVm(String shortName) {
       this.shortName = shortName;
     }
@@ -448,7 +452,9 @@
       return ProguardConfiguration.defaultConfiguration(factory);
     }
     ProguardConfigurationParser parser = new ProguardConfigurationParser(factory);
-    parser.parse(configPaths);
+    for (Path configPath : configPaths) {
+      parser.parse(configPath);
+    }
     return parser.getConfig();
   }
 
@@ -597,13 +603,31 @@
         .toArray(new String[0]));
   }
 
+  public static ProcessResult forkGenerateMainDexList(Path dir, List<String> args1, String... args2)
+      throws IOException, InterruptedException {
+    List<String> args = new ArrayList<>();
+    args.addAll(args1);
+    args.addAll(Arrays.asList(args2));
+    return forkJava(dir, GenerateMainDexList.class, args);
+  }
+
+  public static ProcessResult forkGenerateMainDexList(Path dir, String... args)
+      throws IOException, InterruptedException {
+    return forkJava(dir, GenerateMainDexList.class, args);
+  }
+
   private static ProcessResult forkJava(Path dir, Class clazz, String... args)
       throws IOException, InterruptedException {
+    return forkJava(dir, clazz, Arrays.asList(args));
+  }
+
+  private static ProcessResult forkJava(Path dir, Class clazz, List<String> args)
+      throws IOException, InterruptedException {
     List<String> command = new ImmutableList.Builder<String>()
         .add(getJavaExecutable())
         .add("-cp").add(System.getProperty("java.class.path"))
         .add(clazz.getCanonicalName())
-        .addAll(Arrays.asList(args))
+        .addAll(args)
         .build();
     return runProcess(new ProcessBuilder(command).directory(dir.toFile()));
   }
diff --git a/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
new file mode 100644
index 0000000..e90bca7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/ClassInitializationTest.java
@@ -0,0 +1,110 @@
+// 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.debug;
+
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Test;
+
+public class ClassInitializationTest extends DebugTestBase {
+
+  @Test
+  public void testStaticAssingmentInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerAssignmentInitialization.java";
+    final String CLASS = "ClassInitializerAssignmentInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 7),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 10),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "z", null, Value.createInt(0)),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkStaticField(CLASS, "x", null, Value.createInt(1)),
+        checkStaticField(CLASS, "y", null, Value.createInt(0)),
+        checkStaticField(CLASS, "z", null, Value.createInt(2)),
+        run());
+  }
+
+  @Test
+  public void testBreakpointInEmptyClassInitializer() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerEmpty.java";
+    final String CLASS = "ClassInitializerEmpty";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 8),
+        run());
+  }
+
+  @Test
+  public void testStaticBlockInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerStaticBlockInitialization.java";
+    final String CLASS = "ClassInitializerStaticBlockInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 12),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 13),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 14),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 17),
+        stepOver(),
+        checkLine(SOURCE_FILE, 19),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkLine(SOURCE_FILE, 23),
+        checkStaticField(CLASS, "x", null, Value.createInt(3)),
+        run());
+  }
+
+  @Test
+  public void testStaticMixedInitialization() throws Throwable {
+    final String SOURCE_FILE = "ClassInitializerMixedInitialization.java";
+    final String CLASS = "ClassInitializerMixedInitialization";
+
+    runDebugTest(CLASS,
+        breakpoint(CLASS, "<clinit>"),
+        run(),
+        checkLine(SOURCE_FILE, 8),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(0)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 12),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(1)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 13),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 16),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(0)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 18),
+        checkStaticFieldClinitSafe(CLASS, "x", null, Value.createInt(2)),
+        checkStaticFieldClinitSafe(CLASS, "y", null, Value.createInt(2)),
+        breakpoint(CLASS, "main"),
+        run(),
+        checkLine(SOURCE_FILE, 22),
+        checkStaticField(CLASS, "x", null, Value.createInt(3)),
+        checkStaticField(CLASS, "y", null, Value.createInt(2)),
+        run());
+  }
+}
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 6340728..fa78fd0 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -10,9 +10,12 @@
 import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongList;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -62,8 +65,10 @@
 import org.junit.rules.TestName;
 
 /**
+ * Base class for debugging tests.
  *
- * Base class for debugging tests
+ * The protocol messages are described here:
+ * https://docs.oracle.com/javase/8/docs/platform/jpda/jdwp/jdwp-protocol.html
  */
 public abstract class DebugTestBase {
 
@@ -347,6 +352,27 @@
     });
   }
 
+  protected final JUnit3Wrapper.Command checkStaticFieldClinitSafe(
+      String className, String fieldName, String fieldSignature, Value expectedValue) {
+    return inspect(t -> {
+      // TODO(65148874): The current Art from AOSP master hangs when requesting static fields
+      // when breaking in <clinit>. Last known good version is 7.0.0.
+      Assume.assumeTrue(
+          "Skipping test " + testName.getMethodName() + " because ART version is not supported",
+          isRunningJava() || ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_7_0_0));
+      checkStaticField(className, fieldName, fieldSignature, expectedValue);
+    });
+  }
+
+  protected final JUnit3Wrapper.Command checkStaticField(
+      String className, String fieldName, String fieldSignature, Value expectedValue) {
+    return inspect(t -> {
+      Value value = t.getStaticField(className, fieldName, fieldSignature);
+      Assert.assertEquals("Incorrect value for static '" + className + "." + fieldName + "'",
+          expectedValue, value);
+    });
+  }
+
   protected final JUnit3Wrapper.Command inspect(Consumer<JUnit3Wrapper.DebuggeeState> inspector) {
     return t -> inspector.accept(t.debuggeeState);
   }
@@ -561,6 +587,7 @@
     public static class DebuggeeState implements FrameInspector {
 
       private class DebuggeeFrame implements FrameInspector {
+
         private final long frameId;
         private final Location location;
 
@@ -819,6 +846,77 @@
       public String getMethodSignature() {
         return getTopFrame().getMethodSignature();
       }
+
+      public Value getStaticField(String className, String fieldName, String fieldSignature) {
+        String classSignature = DescriptorUtils.javaTypeToDescriptor(className);
+        byte typeTag = TypeTag.CLASS;
+        long classId = getMirror().getClassID(classSignature);
+        Assert.assertFalse("No class named " + className + " found", classId == -1);
+
+        // The class is available, lookup and read the field.
+        long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+        return getField(getMirror(), classId, fieldId);
+      }
+
+      private long findField(VmMirror mirror, long classId, String fieldName,
+          String fieldSignature) {
+
+        boolean withGenericSignature = true;
+        CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+            ReferenceTypeCommandSet.FieldsWithGenericCommand);
+        commandPacket.setNextValueAsReferenceTypeID(classId);
+        ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+        if (replyPacket.getErrorCode() != Error.NONE) {
+          // Retry with older command ReferenceType.Fields.
+          withGenericSignature = false;
+          commandPacket.setCommand(ReferenceTypeCommandSet.FieldsCommand);
+          replyPacket = mirror.performCommand(commandPacket);
+          assert replyPacket.getErrorCode() == Error.NONE;
+        }
+
+        int fieldsCount = replyPacket.getNextValueAsInt();
+        LongList matchingFieldIds = new LongArrayList();
+        for (int i = 0; i < fieldsCount; ++i) {
+          long currentFieldId = replyPacket.getNextValueAsFieldID();
+          String currentFieldName = replyPacket.getNextValueAsString();
+          String currentFieldSignature = replyPacket.getNextValueAsString();
+          if (withGenericSignature) {
+            replyPacket.getNextValueAsString(); // Skip generic signature.
+          }
+          replyPacket.getNextValueAsInt(); // Skip modifiers.
+
+          // Filter fields based on name (and signature if there is).
+          if (fieldName.equals(currentFieldName)) {
+            if (fieldSignature == null || fieldSignature.equals(currentFieldSignature)) {
+              matchingFieldIds.add(currentFieldId);
+            }
+          }
+        }
+        Assert.assertTrue(replyPacket.isAllDataRead());
+
+        Assert.assertFalse("No field named " + fieldName + " found", matchingFieldIds.isEmpty());
+        // There must be only one matching field.
+        Assert.assertEquals("More than 1 field found: please specify a signature", 1,
+            matchingFieldIds.size());
+        return matchingFieldIds.getLong(0);
+      }
+
+      private Value getField(VmMirror mirror, long classId, long fieldId) {
+
+        CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+            ReferenceTypeCommandSet.GetValuesCommand);
+        commandPacket.setNextValueAsReferenceTypeID(classId);
+        commandPacket.setNextValueAsInt(1);
+        commandPacket.setNextValueAsFieldID(fieldId);
+        ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+        assert replyPacket.getErrorCode() == Error.NONE;
+
+        int fieldsCount = replyPacket.getNextValueAsInt();
+        assert fieldsCount == 1;
+        Value result = replyPacket.getNextValueAsValue();
+        Assert.assertTrue(replyPacket.isAllDataRead());
+        return result;
+      }
     }
 
     private static boolean inScope(long index, Variable var) {
@@ -886,100 +984,6 @@
       setState(State.WaitForEvent);
     }
 
-    private boolean installBreakpoint(BreakpointInfo breakpointInfo) {
-      String classSignature = getClassSignature(breakpointInfo.className);
-      byte typeTag = TypeTag.CLASS;
-      long classId = getMirror().getClassID(classSignature);
-      if (classId == -1) {
-        // Is it an interface ?
-        classId = getMirror().getInterfaceID(classSignature);
-        typeTag = TypeTag.INTERFACE;
-      }
-      if (classId == -1) {
-        // The class is not ready yet. Request a CLASS_PREPARE to delay the installation of the
-        // breakpoint.
-        ReplyPacket replyPacket = getMirror().setClassPrepared(breakpointInfo.className);
-        int classPrepareRequestId = replyPacket.getNextValueAsInt();
-        assertAllDataRead(replyPacket);
-        events.put(Integer.valueOf(classPrepareRequestId),
-            new ClassPrepareHandler(breakpointInfo, classPrepareRequestId));
-        return false;
-      } else {
-        // Find the method.
-        long breakpointMethodId = findMethod(classId, breakpointInfo.methodName,
-            breakpointInfo.methodSignature);
-        long index = getMethodFirstCodeIndex(classId, breakpointMethodId);
-        Assert.assertTrue("No code in method", index >= 0);
-        // Install the breakpoint.
-        ReplyPacket replyPacket = getMirror()
-            .setBreakpoint(new Location(typeTag, classId, breakpointMethodId, index),
-                SuspendPolicy.ALL);
-        checkReplyPacket(replyPacket, "Breakpoint");
-        int breakpointId = replyPacket.getNextValueAsInt();
-        // Nothing to do on breakpoint
-        events.put(Integer.valueOf(breakpointId), new DefaultEventHandler());
-        return true;
-      }
-    }
-
-    private long findMethod(long classId, String methodName, String methodSignature) {
-      class MethodInfo {
-
-        final long methodId;
-        final String methodName;
-        final String methodSignature;
-
-        MethodInfo(long methodId, String methodName, String methodSignature) {
-          this.methodId = methodId;
-          this.methodName = methodName;
-          this.methodSignature = methodSignature;
-        }
-      }
-
-      boolean withGenericSignature = true;
-      CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
-          ReferenceTypeCommandSet.MethodsWithGenericCommand);
-      commandPacket.setNextValueAsReferenceTypeID(classId);
-      ReplyPacket replyPacket = getMirror().performCommand(commandPacket);
-      if (replyPacket.getErrorCode() != Error.NONE) {
-        // Retry with older command ReferenceType.Methods
-        withGenericSignature = false;
-        commandPacket.setCommand(ReferenceTypeCommandSet.MethodsCommand);
-        replyPacket = getMirror().performCommand(commandPacket);
-        assert replyPacket.getErrorCode() == Error.NONE;
-      }
-
-      int methodsCount = replyPacket.getNextValueAsInt();
-      List<MethodInfo> methodInfos = new ArrayList<>(methodsCount);
-      for (int i = 0; i < methodsCount; ++i) {
-        long currentMethodId = replyPacket.getNextValueAsMethodID();
-        String currentMethodName = replyPacket.getNextValueAsString();
-        String currentMethodSignature = replyPacket.getNextValueAsString();
-        if (withGenericSignature) {
-          replyPacket.getNextValueAsString(); // skip generic signature
-        }
-        replyPacket.getNextValueAsInt(); // skip modifiers
-        methodInfos
-            .add(new MethodInfo(currentMethodId, currentMethodName, currentMethodSignature));
-      }
-      Assert.assertTrue(replyPacket.isAllDataRead());
-
-      // Only keep methods with the expected name.
-      methodInfos = methodInfos.stream()
-          .filter(m -> m.methodName.equals(methodName)).collect(
-              Collectors.toList());
-      if (methodSignature != null) {
-        methodInfos = methodInfos.stream()
-            .filter(m -> methodSignature.equals(m.methodSignature)).collect(
-                Collectors.toList());
-      }
-      Assert.assertFalse("No method found", methodInfos.isEmpty());
-      // There must be only one matching method
-      Assert.assertEquals("More than 1 method found: please specify a signature", 1,
-          methodInfos.size());
-      return methodInfos.get(0).methodId;
-    }
-
     private long getMethodFirstCodeIndex(long classId, long breakpointMethodId) {
       ReplyPacket replyPacket = getMirror().getLineTable(classId, breakpointMethodId);
       checkReplyPacket(replyPacket, "Failed to get method line table");
@@ -1019,6 +1023,7 @@
         private final String className;
         private final String methodName;
         private final String methodSignature;
+        private boolean requestedClassPrepare = false;
 
         public BreakpointCommand(String className, String methodName,
             String methodSignature) {
@@ -1031,7 +1036,91 @@
 
         @Override
         public void perform(JUnit3Wrapper testBase) {
-          testBase.installBreakpoint(new BreakpointInfo(className, methodName, methodSignature));
+          VmMirror mirror = testBase.getMirror();
+          String classSignature = getClassSignature(className);
+          byte typeTag = TypeTag.CLASS;
+          long classId = mirror.getClassID(classSignature);
+          if (classId == -1) {
+            // Is it an interface ?
+            classId = mirror.getInterfaceID(classSignature);
+            typeTag = TypeTag.INTERFACE;
+          }
+          if (classId == -1) {
+            // The class is not ready yet. Request a CLASS_PREPARE to delay the installation of the
+            // breakpoint.
+            assert requestedClassPrepare == false : "Already requested class prepare";
+            requestedClassPrepare = true;
+            ReplyPacket replyPacket = mirror.setClassPrepared(className);
+            final int classPrepareRequestId = replyPacket.getNextValueAsInt();
+            testBase.events.put(Integer.valueOf(classPrepareRequestId), wrapper -> {
+              // Remove the CLASS_PREPARE
+              wrapper.events.remove(Integer.valueOf(classPrepareRequestId));
+              wrapper.getMirror().clearEvent(JDWPConstants.EventKind.CLASS_PREPARE,
+                  classPrepareRequestId);
+
+              // Breakpoint then resume. Note: we add them at the beginning of the queue (to be the
+              // next commands to be processed), thus they need to be pushed in reverse order.
+              wrapper.commandsQueue.addFirst(new JUnit3Wrapper.Command.RunCommand());
+              wrapper.commandsQueue.addFirst(BreakpointCommand.this);
+
+              // Set wrapper ready to process next command.
+              wrapper.setState(State.ProcessCommand);
+            });
+          } else {
+            // The class is available: lookup the method then set the breakpoint.
+            long breakpointMethodId = findMethod(mirror, classId, methodName, methodSignature);
+            long index = testBase.getMethodFirstCodeIndex(classId, breakpointMethodId);
+            Assert.assertTrue("No code in method", index >= 0);
+            ReplyPacket replyPacket = testBase.getMirror().setBreakpoint(
+                new Location(typeTag, classId, breakpointMethodId, index), SuspendPolicy.ALL);
+            assert replyPacket.getErrorCode() == Error.NONE;
+            int breakpointId = replyPacket.getNextValueAsInt();
+            testBase.events.put(Integer.valueOf(breakpointId), new DefaultEventHandler());
+          }
+        }
+
+        private static long findMethod(VmMirror mirror, long classId, String methodName,
+            String methodSignature) {
+
+          boolean withGenericSignature = true;
+          CommandPacket commandPacket = new CommandPacket(ReferenceTypeCommandSet.CommandSetID,
+              ReferenceTypeCommandSet.MethodsWithGenericCommand);
+          commandPacket.setNextValueAsReferenceTypeID(classId);
+          ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+          if (replyPacket.getErrorCode() != Error.NONE) {
+            // Retry with older command ReferenceType.Methods
+            withGenericSignature = false;
+            commandPacket.setCommand(ReferenceTypeCommandSet.MethodsCommand);
+            replyPacket = mirror.performCommand(commandPacket);
+            assert replyPacket.getErrorCode() == Error.NONE;
+          }
+
+          int methodsCount = replyPacket.getNextValueAsInt();
+          List<Long> matchingMethodIds = new ArrayList<>();
+          for (int i = 0; i < methodsCount; ++i) {
+            long currentMethodId = replyPacket.getNextValueAsMethodID();
+            String currentMethodName = replyPacket.getNextValueAsString();
+            String currentMethodSignature = replyPacket.getNextValueAsString();
+            if (withGenericSignature) {
+              replyPacket.getNextValueAsString(); // skip generic signature
+            }
+            replyPacket.getNextValueAsInt(); // skip modifiers
+
+            // Filter methods based on name (and signature if there is).
+            if (methodName.equals(currentMethodName)) {
+              if (methodSignature == null || methodSignature.equals(currentMethodSignature)) {
+                matchingMethodIds.add(Long.valueOf(currentMethodId));
+              }
+            }
+          }
+          Assert.assertTrue(replyPacket.isAllDataRead());
+
+          Assert
+              .assertFalse("No method named " + methodName + " found", matchingMethodIds.isEmpty());
+          // There must be only one matching method
+          Assert.assertEquals("More than 1 method found: please specify a signature", 1,
+              matchingMethodIds.size());
+          return matchingMethodIds.get(0);
         }
 
         @Override
@@ -1178,47 +1267,6 @@
       }
     }
 
-    private static class BreakpointInfo {
-
-      private final String className;
-      private final String methodName;
-      private final String methodSignature;
-
-      private BreakpointInfo(String className, String methodName, String methodSignature) {
-        this.className = className;
-        this.methodName = methodName;
-        this.methodSignature = methodSignature;
-      }
-    }
-
-    /**
-     * CLASS_PREPARE signals us that we can install a breakpoint
-     */
-    private static class ClassPrepareHandler implements EventHandler {
-
-      private final BreakpointInfo breakpointInfo;
-      private final int classPrepareRequestId;
-
-      private ClassPrepareHandler(BreakpointInfo breakpointInfo, int classPrepareRequestId) {
-        this.breakpointInfo = breakpointInfo;
-        this.classPrepareRequestId = classPrepareRequestId;
-      }
-
-      @Override
-      public void handle(JUnit3Wrapper testBase) {
-        // Remove the CLASS_PREPARE
-        testBase.events.remove(Integer.valueOf(classPrepareRequestId));
-        testBase.getMirror().clearEvent(JDWPConstants.EventKind.CLASS_PREPARE,
-            classPrepareRequestId);
-
-        // Install breakpoint now.
-        boolean success = testBase.installBreakpoint(breakpointInfo);
-        Assert.assertTrue("Failed to insert breakpoint after class has been prepared", success);
-
-        // Resume now
-        testBase.resume();
-      }
-    }
   }
 
   //
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
new file mode 100644
index 0000000..e62a17b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
@@ -0,0 +1,172 @@
+// 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.debug;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class KotlinInlineTest extends DebugTestBase {
+
+  @Ignore("Requires kotlin-specific stepping behavior")
+  @Test
+  public void testStepOverInline() throws Throwable {
+    String methodName = "singleInline";
+    runDebugTestKotlin("KotlinInline",
+        breakpoint("KotlinInline", methodName),
+        run(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(41, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        // TODO(shertz) stepping over must take kotlin inline range into account.
+        stepOver(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(42, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        stepOver(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(43, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        run());
+  }
+
+  @Ignore("Requires kotlin-specific stepping behavior")
+  @Test
+  public void testStepIntoInline() throws Throwable {
+    String methodName = "singleInline";
+    runDebugTestKotlin("KotlinInline",
+        breakpoint("KotlinInline", methodName),
+        run(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(41, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        // TODO(shertz) stepping over must take kotlin inline range into account.
+        stepInto(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          // The actual line number (the one encoded in debug information) is different than the
+          // source file one.
+          // TODO(shertz) extract original line number from JSR-45's SMAP (only supported on
+          // Android O+).
+          assertTrue(42 != s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        run());
+  }
+
+  @Ignore("Requires kotlin-specific stepping behavior")
+  @Test
+  public void testStepOutInline() throws Throwable {
+    String methodName = "singleInline";
+    runDebugTestKotlin("KotlinInline",
+        breakpoint("KotlinInline", methodName),
+        run(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(41, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        // TODO(shertz) stepping out must take kotlin inline range into account.
+        stepInto(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+        }),
+        stepOut(),
+        inspect(s -> {
+          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(methodName, s.getMethodName());
+          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(43, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        run());
+  }
+
+  @Test
+  public void testKotlinInline() throws Throwable {
+    final String inliningMethodName = "invokeInlinedFunctions";
+    runDebugTestKotlin("KotlinInline",
+        breakpoint("KotlinInline", inliningMethodName),
+        run(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(16, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        stepInto(),
+        inspect(s -> {
+          // We must have stepped into the code of the inlined method but the actual current method
+          // did not change.
+          assertEquals(inliningMethodName, s.getMethodName());
+          // TODO(shertz) get the original line if JSR45 is supported by the targeted ART runtime.
+          s.checkLocal("this");
+        }),
+        stepInto(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(17, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        stepInto(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(18, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("inA", Value.createInt(1));
+          // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
+          s.checkLocal("$i$f$inlinedA");
+          s.checkLocal("$i$a$1$inlinedA");
+        }),
+        stepInto(),
+        inspect(s -> {
+          // We must have stepped into the code of the second inlined method but the actual current
+          // method did not change.
+          assertEquals(inliningMethodName, s.getMethodName());
+          // TODO(shertz) get the original line if JSR45 is supported by the targeted ART runtime.
+          s.checkLocal("this");
+        }),
+        stepInto(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(19, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        stepInto(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(20, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("inB", Value.createInt(2));
+          // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
+          s.checkLocal("$i$f$inlinedB");
+          s.checkLocal("$i$a$1$inlinedB");
+        }),
+        run());
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinTest.java b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
index 8caa3c1..a6b57b8 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinTest.java
@@ -3,97 +3,160 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debug;
 
+import static org.junit.Assert.assertEquals;
+
 import org.apache.harmony.jpda.tests.framework.jdwp.Value;
-import org.junit.Assert;
 import org.junit.Test;
 
 public class KotlinTest extends DebugTestBase {
 
+  // TODO(shertz) simplify test
+  // TODO(shertz) add more variables ?
   @Test
-  public void testKotlinApp() throws Throwable {
-    final String inliningMethodName = "invokeInlinedFunctions";
+  public void testStepOver() throws Throwable {
     runDebugTestKotlin("KotlinApp",
         breakpoint("KotlinApp$Companion", "main"),
         run(),
         inspect(s -> {
-          Assert.assertEquals("KotlinApp.kt", s.getSourceFile());
-          Assert.assertEquals(8, s.getLineNumber());
+          assertEquals("KotlinApp$Companion", s.getClassName());
+          assertEquals("KotlinApp.kt", s.getSourceFile());
+          assertEquals(24, s.getLineNumber());
           s.checkLocal("this");
           s.checkLocal("args");
+          checkNoLocal("instance");
         }),
         stepOver(),
         inspect(s -> {
-          Assert.assertEquals(9, s.getLineNumber());
-          s.checkLocal("this");
-          s.checkLocal("args");
-        }),
-        stepOver(),
-        inspect(s -> {
-          Assert.assertEquals(10, s.getLineNumber());
+          assertEquals(25, s.getLineNumber());
           s.checkLocal("this");
           s.checkLocal("args");
           s.checkLocal("instance");
         }),
         stepOver(),
         inspect(s -> {
-          Assert.assertEquals(11, s.getLineNumber());
+          assertEquals(26, s.getLineNumber());
           s.checkLocal("this");
           s.checkLocal("args");
           s.checkLocal("instance");
         }),
+        run());
+  }
+
+  @Test
+  public void testStepIntoAndOut() throws Throwable {
+    runDebugTestKotlin("KotlinApp",
+        breakpoint("KotlinApp$Companion", "main"),
+        run(),
+        inspect(s -> {
+          assertEquals("KotlinApp$Companion", s.getClassName());
+          assertEquals("KotlinApp.kt", s.getSourceFile());
+          assertEquals(24, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("args");
+          checkNoLocal("instance");
+        }),
+        stepOver(),
+        inspect(s -> {
+          assertEquals(25, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("args");
+          s.checkLocal("instance");
+        }),
+        // Step into 1st invoke of ifElse
         stepInto(),
         inspect(s -> {
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          Assert.assertEquals(24, s.getLineNumber());
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals("KotlinApp.kt", s.getSourceFile());
+          assertEquals(8, s.getLineNumber());
           s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(true));
+          checkNoLocal("a");
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          // We must have stepped into the code of the inlined method but the actual current method
-          // did not change.
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          // TODO(shertz) get the original line if JSR45 is supported by the targeted ART runtime.
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(9, s.getLineNumber());
           s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(true));
+          s.checkLocal("a", Value.createInt(10));
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          Assert.assertEquals(25, s.getLineNumber());
+          // We should be into the 'then' statement.
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(10, s.getLineNumber());
           s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(true));
+          s.checkLocal("a", Value.createInt(10));
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          Assert.assertEquals(26, s.getLineNumber());
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(11, s.getLineNumber());
           s.checkLocal("this");
-          s.checkLocal("inA", Value.createInt(1));
-          // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedA");
-          s.checkLocal("$i$a$1$inlinedA");
+          s.checkLocal("cond", Value.createBoolean(true));
+          s.checkLocal("a", Value.createInt(10));
+          s.checkLocal("b", Value.createInt(20));
+          checkNoLocal("c");
+        }),
+        // Go back to the main method
+        stepOut(),
+        inspect(s -> {
+          assertEquals("KotlinApp$Companion", s.getClassName());
+          assertEquals("KotlinApp.kt", s.getSourceFile());
+          assertEquals(26, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("args");
+          checkNoLocal("instance");
+        }),
+        // Step into 2nd invoke of ifElse
+        stepInto(),
+        inspect(s -> {
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals("KotlinApp.kt", s.getSourceFile());
+          assertEquals(8, s.getLineNumber());
+          s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(false));
+          checkNoLocal("a");
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          // We must have stepped into the code of the second inlined method but the actual current
-          // method did not change.
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          // TODO(shertz) get the original line if JSR45 is supported by the targeted ART runtime.
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(9, s.getLineNumber());
           s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(false));
+          s.checkLocal("a", Value.createInt(10));
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          Assert.assertEquals(27, s.getLineNumber());
+          // We should be into the 'else' statement this time.
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(13, s.getLineNumber());
           s.checkLocal("this");
+          s.checkLocal("cond", Value.createBoolean(false));
+          s.checkLocal("a", Value.createInt(10));
+          checkNoLocal("b");
+          checkNoLocal("c");
         }),
         stepInto(),
         inspect(s -> {
-          Assert.assertEquals(inliningMethodName, s.getMethodName());
-          Assert.assertEquals(28, s.getLineNumber());
+          assertEquals("KotlinApp", s.getClassName());
+          assertEquals(14, s.getLineNumber());
           s.checkLocal("this");
-          s.checkLocal("inB", Value.createInt(2));
-          // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedB");
-          s.checkLocal("$i$a$1$inlinedB");
+          s.checkLocal("cond", Value.createBoolean(false));
+          s.checkLocal("a", Value.createInt(10));
+          checkNoLocal("b");
+          s.checkLocal("c", Value.createInt(5));
         }),
         run());
   }
diff --git a/src/test/java/com/android/tools/r8/debug/LocalsTest.java b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
index 02fb694..12fd23d 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalsTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
@@ -470,4 +470,99 @@
         checkNoLocal("t"),
         run());
   }
+
+  @Test
+  public void argumentLiveAtReturn() throws Throwable {
+    runDebugTest(
+        "Locals",
+        breakpoint("Locals", "argumentLiveAtReturn"),
+        run(),
+        checkLine(SOURCE_FILE, 248),
+        stepOver(),
+        checkLine(SOURCE_FILE, 262),
+        checkLocal("x", Value.createInt(-1)),
+        checkNoLocal("t"),
+        run());
+  }
+
+  @Test
+  public void switchRewriteToIfs() throws Throwable {
+    runDebugTest(
+        "Locals",
+        breakpoint("Locals", "switchRewriteToIfs"),
+        run(),
+        checkLine(SOURCE_FILE, 267),
+        stepOver(),
+        checkLine(SOURCE_FILE, 268),
+        checkLocal("x", Value.createInt(1)),
+        checkLocal("t", Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 269),
+        checkLocal("x", Value.createInt(2)),
+        checkLocal("t", Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 271),
+        checkLocal("x", Value.createInt(4)),
+        checkNoLocal("t"),
+        run());
+  }
+
+  @Test
+  public void switchRewriteToSwitches() throws Throwable {
+    runDebugTest(
+        "Locals",
+        breakpoint("Locals", "switchRewriteToSwitches"),
+        run(),
+        checkLine(SOURCE_FILE, 282),
+        stepOver(),
+        checkLine(SOURCE_FILE, 283),
+        checkLocal("x", Value.createInt(1)),
+        checkLocal("t", Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 284),
+        checkLocal("x", Value.createInt(2)),
+        checkLocal("t", Value.createInt(2)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 286),
+        checkLocal("x", Value.createInt(4)),
+        checkNoLocal("t"),
+        run());
+  }
+
+  @Test
+  public void regression65039701() throws Throwable {
+    runDebugTest(
+        "Locals",
+        breakpoint("Locals", "regression65039701"),
+        run(),
+        checkLine(SOURCE_FILE, 304),
+        checkLocal("createIntNotLong", Value.createBoolean(true)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 305),
+        checkLocal("a"),
+        stepOver(),
+        checkLine(SOURCE_FILE, 306),
+        stepOver(),
+        checkLine(SOURCE_FILE, 308),
+        run());
+  }
+
+  @Test
+  public void regression65066975() throws Throwable {
+    runDebugTest(
+        "Locals",
+        breakpoint("Locals", "regression65066975"),
+        run(),
+        checkLine(SOURCE_FILE, 312),
+        checkLocal("bit", Value.createBoolean(false)),
+        stepOver(),
+        checkLine(SOURCE_FILE, 313),
+        stepOver(),
+        checkLine(SOURCE_FILE, 316),
+        stepOver(),
+        checkLine(SOURCE_FILE, 318),
+        stepOver(),
+        checkLine(SOURCE_FILE, 319),
+        run());
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
index ad6f925..4eae243 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoInspector.java
@@ -11,14 +11,18 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexDebugEntry;
 import com.android.tools.r8.graph.DexDebugEntryBuilder;
+import com.android.tools.r8.graph.DexDebugEvent;
 import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexString;
 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.StringUtils;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -37,8 +41,12 @@
   public DebugInfoInspector(DexEncodedMethod method, DexItemFactory factory) {
     this.method = method;
     info = method.getCode().asDexCode().getDebugInfo();
-    entries = new DexDebugEntryBuilder(method, factory).build();
-    checkConsistentEntries();
+    if (info != null) {
+      entries = new DexDebugEntryBuilder(method, factory).build();
+      checkConsistentEntries();
+    } else {
+      entries = Collections.emptyList();
+    }
   }
 
   public DebugInfoInspector(DexInspector inspector, String clazz, MethodSignature method) {
@@ -50,6 +58,24 @@
     this(new DexInspector(app), clazz, method);
   }
 
+  public boolean hasLocalsInfo() {
+    DexDebugInfo dexInfo = method.getCode().asDexCode().getDebugInfo();
+    if (info == null || dexInfo == null) {
+      return false;
+    }
+    for (DexString parameter : dexInfo.parameters) {
+      if (parameter != null) {
+        return true;
+      }
+    }
+    for (DexDebugEvent event : dexInfo.events) {
+      if (event instanceof DexDebugEvent.StartLocal) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public void checkStartLine(int i) {
     assertEquals(i, info.startLine);
   }
@@ -102,7 +128,8 @@
     DexDebugEntry previousEntry = null;
     for (DexDebugEntry entry : entries) {
       if (previousEntry != null) {
-        assertTrue("More than one entry defined for PC " + entry.address,
+        assertTrue(
+            "More than one entry defined for PC " + StringUtils.hexString(entry.address, 2),
             entry.address > previousEntry.address);
       }
       previousEntry = entry;
diff --git a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTest.java
index be0a122..74992e9 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTest.java
@@ -19,8 +19,43 @@
     return -Math.abs(x);
   }
 
+  private static synchronized int throwing(int cond) {
+    int x = 42;
+    if (cond < 0) {
+      throw new IllegalStateException();
+    }
+    return 2;
+  }
+
+  private static synchronized int monitorExitRegression(int cond) {
+    int x = 42;
+    switch (cond) {
+      case 1:
+        return 1;
+      case 2:
+        throw new IllegalStateException();
+      case 3:
+        throw new RuntimeException();
+      case 4:
+        return 2;
+      case 5:
+        x = 7;
+      case 6:
+        return 3;
+      default:
+    }
+    if (cond > 0) {
+      x = cond + cond;
+    } else {
+      throw new ArithmeticException();
+    }
+    return 2;
+  }
+
   public static void main(String[] args) {
     System.out.println(syncStatic(1234));
     System.out.println(new SynchronizedMethodTest().syncInstance(1234));
+    System.out.println(throwing(1234));
+    System.out.println(monitorExitRegression(1234));
   }
 }
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 7cee4a3..ba0849b 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
@@ -5,8 +5,9 @@
 
 import static org.junit.Assert.assertEquals;
 
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.Arrays;
 import org.junit.Test;
 
 public class SynchronizedMethodTestRunner extends DebugInfoTestBase {
@@ -18,7 +19,7 @@
     AndroidApp d8App = compileWithD8(clazz);
     AndroidApp dxApp = getDxCompiledSources();
 
-    String expected = "42" + ToolHelper.LINE_SEPARATOR + "42" + ToolHelper.LINE_SEPARATOR;
+    String expected = StringUtils.lines("42", "42", "2", "2");
     assertEquals(expected, runOnJava(clazz));
     assertEquals(expected, runOnArt(d8App, clazz.getCanonicalName()));
     assertEquals(expected, runOnArt(dxApp, clazz.getCanonicalName()));
@@ -28,6 +29,14 @@
 
     checkSyncInstance(inspectMethod(d8App, clazz, "int", "syncInstance", "int"));
     checkSyncInstance(inspectMethod(dxApp, clazz, "int", "syncInstance", "int"));
+
+    checkThrowing(inspectMethod(d8App, clazz, "int", "throwing", "int"), false);
+    checkThrowing(inspectMethod(dxApp, clazz, "int", "throwing", "int"), true);
+
+    checkMonitorExitRegression(
+        inspectMethod(d8App, clazz, "int", "monitorExitRegression", "int"), false);
+    checkMonitorExitRegression(
+        inspectMethod(dxApp, clazz, "int", "monitorExitRegression", "int"), true);
   }
 
   private void checkSyncStatic(DebugInfoInspector info) {
@@ -48,4 +57,25 @@
     info.checkLineHasExactLocals(19, locals);
     info.checkNoLine(20);
   }
+
+  private void checkThrowing(DebugInfoInspector info, boolean dx) {
+    info.checkStartLine(23);
+    if (!dx) {
+      info.checkLineHasExactLocals(23, "cond", "int");
+    }
+    info.checkLineHasExactLocals(24, "cond", "int", "x", "int");
+    info.checkLineHasExactLocals(25, "cond", "int", "x", "int");
+    info.checkNoLine(26);
+    info.checkLineHasExactLocals(27, "cond", "int", "x", "int");
+  }
+
+  private void checkMonitorExitRegression(DebugInfoInspector info, boolean dx) {
+    info.checkStartLine(31);
+    for (int line : Arrays.asList(32, 34, 36, 38, 40, 42, 44, 48, 50, 52)) {
+      if (dx && line == 40) {
+        continue;
+      }
+      info.checkLineHasExactLocals(line, "cond", "int", "x", "int");
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
index f731e39..e7718e1 100644
--- a/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/graph/TargetLookupTest.java
@@ -16,7 +16,7 @@
 public class TargetLookupTest extends SmaliTestBase {
 
   @Test
-  public void lookupDirect() {
+  public void lookupDirect() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addDefaultConstructor();
@@ -71,7 +71,7 @@
   }
 
   @Test
-  public void lookupDirectSuper() {
+  public void lookupDirectSuper() throws Exception {
     SmaliBuilder builder = new SmaliBuilder("TestSuper");
 
     builder.addDefaultConstructor();
diff --git a/src/test/java/com/android/tools/r8/internal/D8FrameworkDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/D8FrameworkDeterministicTest.java
new file mode 100644
index 0000000..2cbd60a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/D8FrameworkDeterministicTest.java
@@ -0,0 +1,50 @@
+// 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.internal;
+
+import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.AndroidApp;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+
+public class D8FrameworkDeterministicTest extends CompilationTestBase {
+  private static final int MIN_SDK = 24;
+  private static final String JAR = "third_party/framework/framework_160115954.jar";
+
+  private AndroidApp doRun(D8Command command) throws IOException, CompilationException {
+    return ToolHelper.runD8(command);
+  }
+
+  @Test
+  public void verifyDebugBuild()
+      throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
+    D8Command command = D8Command.builder()
+        .addProgramFiles(Paths.get(JAR))
+        .setMode(CompilationMode.DEBUG)
+        .setMinApiLevel(MIN_SDK)
+        .build();
+    AndroidApp app1 = doRun(command);
+    AndroidApp app2 = doRun(command);
+    assertIdenticalApplications(app1, app2);
+  }
+
+  @Test
+  public void verifyReleaseBuild()
+      throws ExecutionException, IOException, ProguardRuleParserException, CompilationException {
+    D8Command command = D8Command.builder()
+        .addProgramFiles(Paths.get(JAR))
+        .setMode(CompilationMode.RELEASE)
+        .setMinApiLevel(MIN_SDK)
+        .build();
+    AndroidApp app1 = doRun(command);
+    AndroidApp app2 = doRun(command);
+    assertIdenticalApplications(app1, app2);
+  }
+}
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 bc3d758..75a0275 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -11,19 +11,23 @@
 import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.OutputMode;
+import com.beust.jcommander.internal.Lists;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
 
-  public List<DexEncodedMethod> shuffle(List<DexEncodedMethod> methods) {
-    Collections.shuffle(methods);
-    return methods;
+  public Set<DexEncodedMethod> shuffle(Set<DexEncodedMethod> methods) {
+    List<DexEncodedMethod> toShuffle = Lists.newArrayList(methods);
+    Collections.shuffle(toShuffle);
+    return new LinkedHashSet<>(toShuffle);
   }
 
   private AndroidApp doRun()
diff --git a/src/test/java/com/android/tools/r8/jasmin/DebugLocalTests.java b/src/test/java/com/android/tools/r8/jasmin/DebugLocalTests.java
index 2782c91..531831b 100644
--- a/src/test/java/com/android/tools/r8/jasmin/DebugLocalTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/DebugLocalTests.java
@@ -336,4 +336,329 @@
     info.checkLineHasExactLocals(8, "param", "int", "x", "int");
     info.checkLineHasExactLocals(9, "param", "int");
   }
+
+  @Test
+  public void argumentLiveAtReturn() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    /*
+     This is the original Java source code.
+
+     public static int argumentLiveAtReturn(int x) {  // Line 1
+       switch (x) {
+         case 0:
+           return 0;
+         case 1:
+           return 0;
+         case 2:
+           return 0;
+         case 100:
+           return 1;
+         case 101:
+           return 1;
+         case 102:
+           return 1;
+       }
+       return -1;
+     }
+   */
+    MethodSignature foo = clazz.addStaticMethod("argumentLiveAtReturn", ImmutableList.of("I"), "I",
+        ".limit stack 2",
+        ".limit locals 1",
+        ".var 0 is x I from L0 to L8",
+        "L0:",
+        ".line 2",
+        "  iload 0",
+        "lookupswitch",
+        "  0: L1",
+        "  1: L2",
+        "  2: L3",
+        "  100: L4",
+        "  101: L5",
+        "  102: L6",
+        "  default: L7",
+        "L1:",
+        ".line 4",
+        "  iconst_0",
+        "  ireturn",
+        "L2:",
+        ".line 6",
+        "  iconst_0",
+        "  ireturn",
+        "L3:",
+        ".line 8",
+        "  iconst_0",
+        "  ireturn",
+        "L4:",
+        ".line 10",
+        "  iconst_1",
+        "  ireturn",
+        "L5:",
+        ".line 12",
+        "  iconst_1",
+        "  ireturn",
+        "L6:",
+        ".line 14",
+        "  iconst_1",
+        "  ireturn",
+        "L7:",
+        ".line 16",
+        "  iconst_m1",
+        "  ireturn",
+        "L8:"
+    );
+
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc -1",
+        "  invokestatic Test/argumentLiveAtReturn(I)I",
+        "  invokevirtual java/io/PrintStream/print(I)V",
+        "  return");
+
+    String expected = "-1";
+    String javaResult = runOnJava(builder, clazz.name);
+    assertEquals(expected, javaResult);
+
+    AndroidApp jasminApp = builder.build();
+    AndroidApp d8App = ToolHelper.runD8(jasminApp);
+    String artResult = runOnArt(d8App, clazz.name);
+    assertEquals(expected, artResult);
+    DebugInfoInspector info = new DebugInfoInspector(d8App, clazz.name, foo);
+    info.checkStartLine(2);
+    info.checkLineHasExactLocals(2, "x", "int");
+    info.checkLineHasExactLocals(4, "x", "int");
+    info.checkLineHasExactLocals(6, "x", "int");
+    info.checkLineHasExactLocals(8, "x", "int");
+    info.checkLineHasExactLocals(10, "x", "int");
+    info.checkLineHasExactLocals(12, "x", "int");
+    info.checkLineHasExactLocals(14, "x", "int");
+    info.checkLineHasExactLocals(16, "x", "int");
+  }
+
+  @Test
+  public void testLocalSwitchRewriteToIfs() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    /*
+      This is the original Java source code. The code generated by javac have been
+      slightly modified below to end local t on the lookupswitch instruction.
+
+      public static int switchRewrite(int x) {  // Line 1
+        {
+          int t = x + 1;
+          x = t;
+          x = x + x;
+        }
+        switch (x) {
+          case 1:
+            return 7;
+          case 100:
+            return 8;
+        }
+        return -1;
+      }
+    */
+    MethodSignature foo = clazz.addStaticMethod("switchRewrite", ImmutableList.of("I"), "I",
+        ".limit stack 2",
+        ".limit locals 2",
+        ".var 0 is x I from L0 to L7",
+        ".var 1 is t I from L1 to L3",
+
+        "L0:",
+        ".line 3",
+        "  iload 0",
+        "  iconst_1",
+        "  iadd",
+        "  istore 1",
+        "L1:",
+        ".line 4",
+        "  iload 1",
+        "  istore 0",
+        "L2:",
+        ".line 5",
+        "  iload 0",
+        "  iload 0",
+        "  iadd",
+        "  istore 0",
+        "L3_ORIGINAL:",  // This is where javac normally ends t.
+        ".line 7",
+        "  iload 0",
+        "L3:",           // Moved L3 here to end t on the switch instruction.
+        "lookupswitch",
+        "  0: L4",
+        "  100: L5",
+        "  default: L6",
+        "L4:",
+        ".line 9",
+        "  iconst_0",
+        "  ireturn",
+        "L5:",
+        ".line 11",
+        "  iconst_1",
+        "  ireturn",
+        "L6:",
+        ".line 13",
+        "  iconst_m1",
+        "  ireturn",
+        "L7:"
+    );
+
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc 1",
+        "  invokestatic Test/switchRewrite(I)I",
+        "  invokevirtual java/io/PrintStream/print(I)V",
+        "  return");
+
+    String expected = "-1";
+    String javaResult = runOnJava(builder, clazz.name);
+    assertEquals(expected, javaResult);
+
+    AndroidApp jasminApp = builder.build();
+    AndroidApp d8App = ToolHelper.runD8(jasminApp);
+    String artResult = runOnArt(d8App, clazz.name);
+    assertEquals(expected, artResult);
+
+    DebugInfoInspector info = new DebugInfoInspector(d8App, clazz.name, foo);
+    info.checkStartLine(3);
+    info.checkLineHasExactLocals(3, "x", "int");
+    info.checkLineHasExactLocals(4, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(5, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(7, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(9, "x", "int");
+    info.checkLineHasExactLocals(11, "x", "int");
+    info.checkLineHasExactLocals(13, "x", "int");
+  }
+
+  @Test
+  public void testLocalSwitchRewriteToSwitches() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    /*
+      This is the original Java source code. The code generated by javac have been
+      slightly modified below to end local t on the lookupswitch instruction.
+
+      public static int switchRewrite(int x) {  // Line 1
+        {
+          int t = x + 1;
+          x = t;
+          x = x + x;
+        }
+        switch (x) {
+          case 0:
+            return 0;
+          case 1:
+            return 0;
+          case 2:
+            return 0;
+          case 100:
+            return 1;
+          case 101:
+            return 1;
+          case 102:
+            return 1;
+        }
+        return -1;
+      }
+    */
+    MethodSignature foo = clazz.addStaticMethod("switchRewrite", ImmutableList.of("I"), "I",
+        ".limit stack 2",
+        ".limit locals 2",
+        ".var 0 is x I from L0 to L11",
+        ".var 1 is t I from L1 to L3",
+
+        "L0:",
+        ".line 3",
+        "  iload 0",
+        "  iconst_1",
+        "  iadd",
+        "  istore 1",
+        "L1:",
+        ".line 4",
+        "  iload 1",
+        "  istore 0",
+        "L2:",
+        ".line 5",
+        "  iload 0",
+        "  iload 0",
+        "  iadd",
+        "  istore 0",
+        "L3_ORIGINAL:",  // This is where javac normally ends t.
+        ".line 7",
+        "  iload 0",
+        "L3:",           // Moved L3 here to end t on the switch instruction.
+        "lookupswitch",
+        "  0: L4",
+        "  1: L5",
+        "  2: L6",
+        "  100: L7",
+        "  101: L8",
+        "  102: L9",
+        "  default: L10",
+        "L4:",
+        ".line 9",
+        "  iconst_0",
+        "  ireturn",
+        "L5:",
+        ".line 11",
+        "  iconst_0",
+        "  ireturn",
+        "L6:",
+        ".line 13",
+        "  iconst_0",
+        "  ireturn",
+        "L7:",
+        ".line 15",
+        "  iconst_1",
+        "  ireturn",
+        "L8:",
+        ".line 17",
+        "  iconst_1",
+        "  ireturn",
+        "L9:",
+        ".line 19",
+        "  iconst_1",
+        "  ireturn",
+        "L10:",
+        ".line 21",
+        "  iconst_m1",
+        "  ireturn",
+        "L11:"
+    );
+
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc 1",
+        "  invokestatic Test/switchRewrite(I)I",
+        "  invokevirtual java/io/PrintStream/print(I)V",
+        "  return");
+
+    String expected = "-1";
+    String javaResult = runOnJava(builder, clazz.name);
+    assertEquals(expected, javaResult);
+
+    AndroidApp jasminApp = builder.build();
+    AndroidApp d8App = ToolHelper.runD8(jasminApp);
+    String artResult = runOnArt(d8App, clazz.name);
+    assertEquals(expected, artResult);
+
+    DebugInfoInspector info = new DebugInfoInspector(d8App, clazz.name, foo);
+    info.checkStartLine(3);
+    info.checkLineHasExactLocals(3, "x", "int");
+    info.checkLineHasExactLocals(4, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(5, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(7, "x", "int", "t", "int");
+    info.checkLineHasExactLocals(9, "x", "int");
+    info.checkLineHasExactLocals(11, "x", "int");
+    info.checkLineHasExactLocals(13, "x", "int");
+  }
 }
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 81d299e..2551941 100644
--- a/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/InvalidDebugInfoTests.java
@@ -4,8 +4,13 @@
 package com.android.tools.r8.jasmin;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.debuginfo.DebugInfoInspector;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.ImmutableList;
 import org.junit.Test;
@@ -128,7 +133,7 @@
   public void invalidInfoBug63412730_onWrite() throws Throwable {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
-    clazz.addStaticMethod("bar", ImmutableList.of(), "V",
+    MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V",
         ".limit stack 3",
         ".limit locals 2",
         ".var 1 is i I from LI to End",
@@ -136,9 +141,11 @@
         "Init:",
         "  ldc 42",
         "  istore 0",
+        ".line 1",
         "LI:",
         "  ldc 7.5",
         "  fstore 1",
+        ".line 2",
         "LF:",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
         "  dup",
@@ -146,6 +153,7 @@
         "  invokevirtual java/io/PrintStream/println(I)V",
         "  fload 1",
         "  invokevirtual java/io/PrintStream/println(F)V",
+        ".line 3",
         "  return",
         "End:");
 
@@ -158,15 +166,18 @@
     String expected = StringUtils.lines("42", "7.5");
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
-    String artResult = runOnArtD8(builder, clazz.name);
+    AndroidApp app = compileWithD8(builder);
+    String artResult = runOnArt(app, clazz.name);
     assertEquals(expected, artResult);
+    DebugInfoInspector info = new DebugInfoInspector(app, clazz.name, method);
+    assertFalse(info.hasLocalsInfo());
   }
 
   @Test
   public void invalidInfoBug63412730_onRead() throws Throwable {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
-    clazz.addStaticMethod("bar", ImmutableList.of(), "V",
+    MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V",
         ".limit stack 3",
         ".limit locals 2",
         ".var 1 is i I from Locals to End",
@@ -174,8 +185,10 @@
         "Init:",
         "  ldc 42",
         "  istore 0",
+        ".line 1",
         "  ldc 7.5",
         "  fstore 1",
+        ".line 2",
         "Locals:",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
         "  dup",
@@ -183,6 +196,7 @@
         "  invokevirtual java/io/PrintStream/println(I)V",
         "  fload 1",
         "  invokevirtual java/io/PrintStream/println(F)V",
+        ".line 3",
         "  return",
         "End:");
 
@@ -195,15 +209,18 @@
     String expected = StringUtils.lines("42", "7.5");
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
-    String artResult = runOnArtD8(builder, clazz.name);
+    AndroidApp app = compileWithD8(builder);
+    String artResult = runOnArt(app, clazz.name);
     assertEquals(expected, artResult);
+    DebugInfoInspector info = new DebugInfoInspector(app, clazz.name, method);
+    assertFalse(info.hasLocalsInfo());
   }
 
   @Test
   public void invalidInfoBug63412730_onMove() throws Throwable {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
-    clazz.addStaticMethod("bar", ImmutableList.of(), "V",
+    MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V",
         ".limit stack 3",
         ".limit locals 2",
         ".var 1 is i I from LI to End",
@@ -211,21 +228,25 @@
         "Init:",
         "  ldc 42",
         "  istore 0",
+        ".line 1",
         "LI:",
         "  ldc 0",
         "  ldc 0",
         "  ifeq LZ",
         "  ldc 75",
         "  istore 1",
+        ".line 2",
         "LJ:",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
         "  iload 1",
         "  invokevirtual java/io/PrintStream/println(I)V",
+        ".line 3",
         "  return",
         "LZ:",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
         "  iload 0",
         "  invokevirtual java/io/PrintStream/println(I)V",
+        ".line 4",
         "  return",
         "End:");
 
@@ -238,15 +259,18 @@
     String expected = StringUtils.lines("42");
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
-    String artResult = runOnArtD8(builder, clazz.name);
+    AndroidApp app = compileWithD8(builder);
+    String artResult = runOnArt(app, clazz.name);
     assertEquals(expected, artResult);
+    DebugInfoInspector info = new DebugInfoInspector(app, clazz.name, method);
+    assertFalse(info.hasLocalsInfo());
   }
 
   @Test
   public void invalidInfoBug63412730_onPop() throws Throwable {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
-    clazz.addStaticMethod("bar", ImmutableList.of(), "V",
+    MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V",
         ".limit stack 3",
         ".limit locals 2",
         ".var 1 is a [Ljava/lang/Object; from Locals to End",
@@ -255,21 +279,25 @@
         "  ldc 1",
         "  anewarray java/lang/Object",
         "  astore 0",
+        ".line 1",
         "  new java/lang/Integer",
         "  dup",
         "  ldc 42",
         "  invokespecial java/lang/Integer/<init>(I)V",
         "  astore 1",
+        ".line 2",
         "Locals:",
         "  aload 0",
         "  ldc 0",
         "  aload 1",
         "  aastore",
+        ".line 3",
         "  getstatic java/lang/System/out Ljava/io/PrintStream;",
         "  aload 0",
         "  ldc 0",
         "  aaload",
         "  invokevirtual java/io/PrintStream/println(Ljava/lang/Object;)V",
+        ".line 4",
         "  return",
         "End:");
 
@@ -282,7 +310,12 @@
     String expected = StringUtils.lines("42");
     String javaResult = runOnJava(builder, clazz.name);
     assertEquals(expected, javaResult);
-    String artResult = runOnArtD8(builder, clazz.name);
+    AndroidApp app = compileWithD8(builder);
+    String artResult = runOnArt(app, clazz.name);
     assertEquals(expected, artResult);
+    DebugInfoInspector info = new DebugInfoInspector(app, clazz.name, method);
+    // Note: This code is actually invalid debug info, but we do not reject it because both types
+    // are reference types. If we ever change that we should update this test.
+    assertTrue(info.hasLocalsInfo());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/jasmin/Regress65007724.java b/src/test/java/com/android/tools/r8/jasmin/Regress65007724.java
new file mode 100644
index 0000000..1eb0586
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/Regress65007724.java
@@ -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.
+package com.android.tools.r8.jasmin;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class Regress65007724 extends JasminTestBase {
+  @Test
+  public void testThat16BitsIndexAreAllowed() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    for (int i = 0; i < 35000; i++) {
+      builder.addClass("C" + i);
+    }
+
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    clazz.addStaticField("f", "LC34000;", null);
+
+    clazz.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "ldc \"Hello World!\"",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
+        "return");
+
+    String expected = runOnJava(builder, clazz.name);
+    String artResult = runOnArtD8(builder, clazz.name);
+    assertEquals(expected, artResult);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/jsr45/JSR45Tests.java b/src/test/java/com/android/tools/r8/jsr45/JSR45Tests.java
index d5f9c6a..601ca23 100644
--- a/src/test/java/com/android/tools/r8/jsr45/JSR45Tests.java
+++ b/src/test/java/com/android/tools/r8/jsr45/JSR45Tests.java
@@ -60,7 +60,7 @@
   }
 
   void compileWithR8(Path inputPath, Path outputPath, Path keepRulesPath)
-      throws IOException, CompilationException, ExecutionException, ProguardRuleParserException {
+      throws IOException, CompilationException, ProguardRuleParserException {
     AndroidApp androidApp =
         R8.run(
             R8Command.builder()
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
index dfc20f9..1039475 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListOutputTest.java
@@ -38,7 +38,7 @@
     Path mainDexListOutput = temp.getRoot().toPath().resolve("main-dex-output.txt");
     R8Command command =
         ToolHelper.prepareR8CommandBuilder(readClasses(HelloWorldMain.class))
-            .addMainDexRules(mainDexRules)
+            .addMainDexRulesFiles(mainDexRules)
             .setMainDexListOutputPath(mainDexListOutput)
             .build();
     ToolHelper.runR8(command);
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 619f808..4122416 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -19,7 +19,7 @@
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.MainDexError;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.Code;
@@ -36,6 +36,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.DebugPosition;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.SourceCode;
@@ -161,7 +162,7 @@
     try {
       verifyMainDexContains(TWO_LARGE_CLASSES, getTwoLargeClassesAppPath(), false);
       fail("Expect to fail, for there are too many classes for the main-dex list.");
-    } catch (CompilationError e) {
+    } catch (DexOverflowException e) {
       // Make sure {@link MonoDexDistributor} was _not_ used.
       assertFalse(e.getMessage().contains("single dex file"));
       // Make sure what exceeds the limit is the number of methods.
@@ -202,7 +203,7 @@
     try {
       verifyMainDexContains(MANY_CLASSES, getManyClassesMultiDexAppPath(), false);
       fail("Expect to fail, for there are too many classes for the main-dex list.");
-    } catch (CompilationError e) {
+    } catch (DexOverflowException e) {
       // Make sure {@link MonoDexDistributor} was _not_ used.
       assertFalse(e.getMessage().contains("single dex file"));
       // Make sure what exceeds the limit is the number of methods.
@@ -402,7 +403,7 @@
       generateApplication(
           MANY_CLASSES, Constants.ANDROID_K_API, false, MANY_CLASSES_MULTI_DEX_METHODS_PER_CLASS);
       fail("Expect to fail, for there are many classes while multidex is not enabled.");
-    } catch (MainDexError e) {
+    } catch (DexOverflowException e) {
       // Make sure {@link MonoDexDistributor} was used.
       assertTrue(e.getMessage().contains("single dex file"));
       // Make sure what exceeds the limit is the number of methods.
@@ -689,6 +690,16 @@
     }
 
     @Override
+    public int getMoveExceptionRegister() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public DebugPosition getDebugPositionAtOffset(int offset) {
+      throw new Unreachable();
+    }
+
+    @Override
     public boolean verifyRegister(int register) {
       throw new Unreachable();
     }
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 57c8cf6..ba5092f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexTracingTest.java
@@ -9,16 +9,18 @@
 import static com.android.tools.r8.utils.FileUtils.ZIP_EXTENSION;
 
 import com.android.tools.r8.CompilationResult;
+import com.android.tools.r8.GenerateMainDexList;
+import com.android.tools.r8.GenerateMainDexListCommand;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
@@ -107,7 +109,6 @@
         mainDexRules,
         expectedMainDexList,
         minSdk,
-        R8Command.builder(),
         (options) -> {
           options.inlineAccessors = false;
         });
@@ -120,48 +121,86 @@
       Path mainDexRules,
       Path expectedMainDexList,
       int minSdk,
-      R8Command.Builder builder,
       Consumer<InternalOptions> optionsConsumer)
       throws Throwable {
     Path out = temp.getRoot().toPath().resolve(testName + ZIP_EXTENSION);
 
     Path inputJar = Paths.get(buildDir, packageName + JAR_EXTENSION);
-    builder.setMinApiLevel(minSdk);
     try {
-      R8Command command = builder
+      // Build main-dex list using GenerateMainDexList.
+      GenerateMainDexListCommand.Builder mdlCommandBuilder = GenerateMainDexListCommand.builder();
+      GenerateMainDexListCommand command2 = mdlCommandBuilder
           .addProgramFiles(inputJar)
-          .addLibraryFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION),
-              Paths.get(ToolHelper.getAndroidJar(minSdk)))
+          .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+          .addMainDexRulesFiles(mainDexRules)
+          .build();
+      List<String> mainDexGeneratorMainDexList =
+          GenerateMainDexList.run(command2).stream()
+              .map(this::mainDexStringToDescriptor)
+              .sorted()
+              .collect(Collectors.toList());
+
+      // Build main-dex list using R8.
+      R8Command.Builder r8CommandBuilder = R8Command.builder();
+      R8Command command = r8CommandBuilder
+          .setMinApiLevel(minSdk)
+          .addProgramFiles(inputJar)
+          .addProgramFiles(Paths.get(EXAMPLE_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION))
+          .addLibraryFiles(Paths.get(ToolHelper.getAndroidJar(minSdk)))
           .setOutputPath(out)
-          .addMainDexRules(mainDexRules)
+          .addMainDexRulesFiles(mainDexRules)
           .build();
       CompilationResult result = ToolHelper.runR8WithFullResult(command, optionsConsumer);
-      List<String> resultMainDexList =
+      List<String> r8MainDexList =
           result.dexApplication.mainDexList.stream()
               .filter(dexType -> isApplicationClass(dexType, result))
               .map(dexType -> dexType.descriptor.toString())
+              .sorted()
               .collect(Collectors.toList());
-      Collections.sort(resultMainDexList);
+
+      // Check that both generated lists are the same as the reference list, except for lambda
+      // classes which are only produced when running R8.
       String[] refList = new String(Files.readAllBytes(
           expectedMainDexList), StandardCharsets.UTF_8).split("\n");
+      int nonLambdaOffset = 0;
       for (int i = 0; i < refList.length; 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
-          // class. However, we do not check the hash for the generated lambda class. The hash
-          // changes for different compiler versions because different compiler versions generate
-          // different lambda implementation method names.
-          reference = reference.substring(0, reference.lastIndexOf('$'));
-          computed = computed.substring(0, computed.lastIndexOf('$'));
+        checkSameMainDexEntry(reference, r8MainDexList.get(i));
+        // The main dex list generator does not do any lambda desugaring.
+        if (!isLambda(reference)) {
+          checkSameMainDexEntry(reference, mainDexGeneratorMainDexList.get(i - nonLambdaOffset));
+        } else {
+          nonLambdaOffset++;
         }
-        Assert.assertEquals(reference, computed);
       }
     } catch (ExecutionException e) {
       throw e.getCause();
     }
   }
 
+  private boolean isLambda(String mainDexEntry) {
+    return mainDexEntry.contains("-$$Lambda$");
+  }
+
+  private String mainDexStringToDescriptor(String mainDexString) {
+    final String CLASS_EXTENSION = ".class";
+    Assert.assertTrue(mainDexString.endsWith(CLASS_EXTENSION));
+    return DescriptorUtils.getDescriptorFromClassBinaryName(
+        mainDexString.substring(0, mainDexString.length() - CLASS_EXTENSION.length()));
+  }
+
+  private void checkSameMainDexEntry(String reference, String computed) {
+    if (isLambda(reference)) {
+      // For lambda classes we check that there is a lambda class for the right containing
+      // class. However, we do not check the hash for the generated lambda class. The hash
+      // changes for different compiler versions because different compiler versions generate
+      // different lambda implementation method names.
+      reference = reference.substring(0, reference.lastIndexOf('$'));
+      computed = computed.substring(0, computed.lastIndexOf('$'));
+    }
+    Assert.assertEquals(reference, computed);
+  }
+
   private boolean isApplicationClass(DexType dexType, CompilationResult result) {
     DexClass clazz = result.appInfo.definitionFor(dexType);
     return clazz != null && clazz.isProgramClass();
diff --git a/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
index 195c3aa..b8d8e58 100644
--- a/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
@@ -7,7 +7,21 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.Sput;
+import com.android.tools.r8.code.SputObject;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueBoolean;
+import com.android.tools.r8.graph.DexValue.DexValueByte;
+import com.android.tools.r8.graph.DexValue.DexValueChar;
+import com.android.tools.r8.graph.DexValue.DexValueDouble;
+import com.android.tools.r8.graph.DexValue.DexValueFloat;
+import com.android.tools.r8.graph.DexValue.DexValueInt;
+import com.android.tools.r8.graph.DexValue.DexValueLong;
+import com.android.tools.r8.graph.DexValue.DexValueShort;
+import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.smali.SmaliTestBase;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
@@ -17,7 +31,7 @@
 public class StaticValuesTest extends SmaliTestBase {
 
   @Test
-  public void testAllTypes() {
+  public void testAllTypes() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticField("booleanField", "Z");
@@ -80,7 +94,55 @@
     DexApplication processedApplication = processApplication(originalApplication, options);
 
     DexInspector inspector = new DexInspector(processedApplication);
-    assertFalse(inspector.clazz("Test").clinit().isPresent());
+    // Test is running without tree-shaking, so the empty <clinit> is not removed.
+    assertTrue(
+        inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
+
+    DexValue value;
+    assertTrue(inspector.clazz("Test").field("boolean", "booleanField").hasStaticValue());
+    value = inspector.clazz("Test").field("boolean", "booleanField").getStaticValue();
+    assertTrue(value instanceof DexValueBoolean);
+    assertEquals(true, ((DexValueBoolean) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("byte", "byteField").hasStaticValue());
+    value = inspector.clazz("Test").field("byte", "byteField").getStaticValue();
+    assertTrue(value instanceof DexValueByte);
+    assertEquals(1, ((DexValueByte) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("short", "shortField").hasStaticValue());
+    value = inspector.clazz("Test").field("short", "shortField").getStaticValue();
+    assertTrue(value instanceof DexValueShort);
+    assertEquals(2, ((DexValueShort) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    value = inspector.clazz("Test").field("int", "intField").getStaticValue();
+    assertTrue(value instanceof DexValueInt);
+    assertEquals(3, ((DexValueInt) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("long", "longField").hasStaticValue());
+    value = inspector.clazz("Test").field("long", "longField").getStaticValue();
+    assertTrue(value instanceof DexValueLong);
+    assertEquals(4, ((DexValueLong) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("float", "floatField").hasStaticValue());
+    value = inspector.clazz("Test").field("float", "floatField").getStaticValue();
+    assertTrue(value instanceof DexValueFloat);
+    assertEquals(5.0f, ((DexValueFloat) value).getValue(), 0.0);
+
+    assertTrue(inspector.clazz("Test").field("double", "doubleField").hasStaticValue());
+    value = inspector.clazz("Test").field("double", "doubleField").getStaticValue();
+    assertTrue(value instanceof DexValueDouble);
+    assertEquals(6.0f, ((DexValueDouble) value).getValue(), 0.0);
+
+    assertTrue(inspector.clazz("Test").field("char", "charField").hasStaticValue());
+    value = inspector.clazz("Test").field("char", "charField").getStaticValue();
+    assertTrue(value instanceof DexValueChar);
+    assertEquals(0x30 + 7, ((DexValueChar) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
+    assertTrue(value instanceof DexValueString);
+    assertEquals(("8"), ((DexValueString) value).getValue().toString());
 
     String result = runArt(processedApplication, options);
 
@@ -88,7 +150,7 @@
   }
 
   @Test
-  public void getBeforePut() {
+  public void getBeforePut() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticField("field1", "I", "1");
@@ -127,7 +189,7 @@
   }
 
   @Test
-  public void testNull() {
+  public void testNull() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticField("stringField", "Ljava/lang/String;", "Hello");
@@ -159,7 +221,9 @@
     DexApplication processedApplication = processApplication(originalApplication, options);
 
     DexInspector inspector = new DexInspector(processedApplication);
-    assertFalse(inspector.clazz("Test").clinit().isPresent());
+    // Test is running without tree-shaking, so the empty <clinit> is not removed.
+    assertTrue(
+        inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
 
     String result = runArt(processedApplication, options);
 
@@ -167,7 +231,7 @@
   }
 
   @Test
-  public void testString() {
+  public void testString() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticField("stringField1", "Ljava/lang/String;", "Hello");
@@ -200,7 +264,9 @@
     DexApplication processedApplication = processApplication(originalApplication, options);
 
     DexInspector inspector = new DexInspector(processedApplication);
-    assertFalse(inspector.clazz("Test").clinit().isPresent());
+    // Test is running without tree-shaking, so the empty <clinit> is not removed.
+    assertTrue(
+        inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
 
     String result = runArt(processedApplication, options);
 
@@ -208,7 +274,153 @@
   }
 
   @Test
-  public void testInitializationToOwnClassName() {
+  public void testMultiplePuts() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
+
+    builder.addStaticField("intField", "I");
+    builder.addStaticField("stringField", "Ljava/lang/String;");
+
+    builder.addStaticInitializer(
+        1,
+        "const               v0, 0",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"4\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "const               v0, 1",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"5\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "const               v0, 2",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"6\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "const               v0, 3",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"7\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "return-void"
+    );
+    builder.addMainMethod(
+        2,
+        "sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "sget                v1, LTest;->intField:I",
+        "invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->println(I)V",
+        "sget-object         v1, LTest;->stringField:Ljava/lang/String;",
+        "invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+        "return-void"
+    );
+
+    InternalOptions options = new InternalOptions();
+    DexApplication originalApplication = buildApplication(builder, options);
+    DexApplication processedApplication = processApplication(originalApplication, options);
+
+    DexInspector inspector = new DexInspector(processedApplication);
+    // Test is running without tree-shaking, so the empty <clinit> is not removed.
+    assertTrue(
+        inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
+
+    DexValue value;
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    value = inspector.clazz("Test").field("int", "intField").getStaticValue();
+    assertTrue(value instanceof DexValueInt);
+    assertEquals(3, ((DexValueInt) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
+    assertTrue(value instanceof DexValueString);
+    assertEquals(("7"), ((DexValueString) value).getValue().toString());
+
+    String result = runArt(processedApplication, options);
+
+    assertEquals("3\n7\n", result);
+  }
+
+
+  @Test
+  public void testMultiplePutsWithControlFlow() throws Exception {
+    SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
+
+    builder.addStaticField("booleanField", "Z");
+    builder.addStaticField("intField", "I");
+    builder.addStaticField("intField2", "I");
+    builder.addStaticField("stringField", "Ljava/lang/String;");
+
+    builder.addStaticInitializer(
+        1,
+        "const               v0, 0",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"4\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "const               v0, 1",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"5\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "sget-boolean        v0, LTest;->booleanField:Z",
+        "if-eqz              v0, :label_1",
+        "const               v0, 8",
+        "sput                v0, LTest;->intField2:I",
+        ":label_1",
+        "const               v0, 9",
+        "sput                v0, LTest;->intField2:I",
+        "goto                :label_2",
+        ":label_2",
+        "const               v0, 2",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"6\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "const               v0, 3",
+        "sput                v0, LTest;->intField:I",
+        "const-string        v0, \"7\"",
+        "sput-object         v0, LTest;->stringField:Ljava/lang/String;",
+        "return-void"
+    );
+    builder.addMainMethod(
+        2,
+        "sget-object         v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+        "sget                v1, LTest;->intField:I",
+        "invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->println(I)V",
+        "sget-object         v1, LTest;->stringField:Ljava/lang/String;",
+        "invoke-virtual      { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+        "return-void"
+    );
+
+    InternalOptions options = new InternalOptions();
+    DexApplication originalApplication = buildApplication(builder, options);
+    DexApplication processedApplication = processApplication(originalApplication, options);
+
+    DexInspector inspector = new DexInspector(processedApplication);
+    assertTrue(inspector.clazz("Test").clinit().isPresent());
+
+    DexValue value;
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    value = inspector.clazz("Test").field("int", "intField").getStaticValue();
+    assertTrue(value instanceof DexValueInt);
+    assertEquals(3, ((DexValueInt) value).getValue());
+
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
+    assertTrue(value instanceof DexValueString);
+    assertEquals(("7"), ((DexValueString) value).getValue().toString());
+
+    DexCode code = inspector.clazz("Test").clinit().getMethod().getCode().asDexCode();
+    for (Instruction instruction : code.instructions) {
+      if (instruction instanceof Sput) {
+        Sput put = (Sput) instruction;
+        // Only int put ot intField2.
+        assertEquals(put.getField().name.toString(), "intField2");
+      } else {
+        // No Object (String) puts.
+        assertFalse(instruction instanceof SputObject);
+      }
+    }
+
+    String result = runArt(processedApplication, options);
+
+    assertEquals("3\n7\n", result);
+  }
+
+  @Test
+  public void testInitializationToOwnClassName() throws Exception {
     String className = "org.example.Test";
     SmaliBuilder builder = new SmaliBuilder(className);
 
@@ -266,7 +478,9 @@
 
     DexInspector inspector = new DexInspector(processedApplication);
     assertTrue(inspector.clazz(className).isPresent());
-    assertFalse(inspector.clazz(className).clinit().isPresent());
+    // Test is running without tree-shaking, so the empty <clinit> is not removed.
+    assertTrue(
+        inspector.clazz(className).clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
 
     String result = runArt(processedApplication, options, className);
 
@@ -275,7 +489,7 @@
   }
 
   @Test
-  public void testInitializationToOtherClassName() {
+  public void testInitializationToOtherClassName() throws Exception {
     String className = "org.example.Test";
     SmaliBuilder builder = new SmaliBuilder(className);
 
@@ -322,7 +536,7 @@
   }
 
   @Test
-  public void fieldOnOtherClass() {
+  public void fieldOnOtherClass() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticInitializer(
diff --git a/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
index 03df65c..4cb6736 100644
--- a/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
@@ -126,7 +126,9 @@
 
   private static void inspectShaking1(PrintUsageInspector inspector) {
     assertTrue(inspector.clazz("shaking1.Unused").isPresent());
-    assertFalse(inspector.clazz("shaking1.Used").isPresent());
+    assertTrue(inspector.clazz("shaking1.Used").isPresent());
+    ClassSubject used = inspector.clazz("shaking1.Used").get();
+    assertTrue(used.method("void", "<clinit>", ImmutableList.of()));
   }
 
   private static void inspectShaking2(PrintUsageInspector inspector) {
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 ac0178c..0f2e456 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexAccessFlags;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -102,11 +103,22 @@
 
   @Test
   public void parse() throws IOException, ProguardRuleParserException {
-    ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory());
+    ProguardConfigurationParser parser;
+
+    // Parse from file.
+    parser = new ProguardConfigurationParser(new DexItemFactory());
     parser.parse(Paths.get(PROGUARD_SPEC_FILE));
     List<ProguardConfigurationRule> rules = parser.getConfig().getRules();
     assertEquals(24, rules.size());
     assertEquals(1, rules.get(0).getMemberRules().size());
+
+    // Parse from strings.
+    parser = new ProguardConfigurationParser(new DexItemFactory());
+    List<String> lines = FileUtils.readTextFile(Paths.get(PROGUARD_SPEC_FILE));
+    parser.parse(new ProguardConfigurationSourceStrings(lines));
+    rules = parser.getConfig().getRules();
+    assertEquals(24, rules.size());
+    assertEquals(1, rules.get(0).getMemberRules().size());
   }
 
   @Test
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 ab6d6e2..66082fc 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -150,6 +150,9 @@
         used.method("java.lang.String", "aMethodThatIsNotUsedButKept", Collections.emptyList())
             .isPresent());
     Assert.assertTrue(used.field("int", "aStaticFieldThatIsNotUsedButKept").isPresent());
+    // Rewriting of <clinit> moves the initialization of aStaticFieldThatIsNotUsedButKept
+    // from <clinit> code into statics value section of the dex file.
+    Assert.assertFalse(used.clinit().isPresent());
   }
 
   public static void shaking1IsCorrectlyRepackaged(DexInspector inspector) {
diff --git a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
index a02a186..5ad5787 100644
--- a/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/includedescriptorclasses/IncludeDescriptorClassesTest.java
@@ -13,15 +13,15 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.google.common.collect.ImmutableList;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
+import java.util.zip.ZipFile;
 import org.junit.Test;
 
 public class IncludeDescriptorClassesTest extends TestBase {
@@ -51,14 +51,14 @@
 
   private Set<String> readJarClasses(Path jar) throws IOException {
     Set<String> result = new HashSet<>();
-    try (ZipInputStream in = new ZipInputStream(new FileInputStream(jar.toFile()))) {
-      ZipEntry entry = in.getNextEntry();
-      while (entry != null) {
+    try (ZipFile zipFile = new ZipFile(jar.toFile())) {
+      final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry entry = entries.nextElement();
         String name = entry.getName();
         if (name.endsWith(".class")) {
           result.add(name.substring(0, name.length() - ".class".length()).replace('/', '.'));
         }
-        entry = in.getNextEntry();
       }
     }
     return result;
diff --git a/src/test/java/com/android/tools/r8/smali/JumboStringTest.java b/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
index 1d256e0..a52c68b 100644
--- a/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
+++ b/src/test/java/com/android/tools/r8/smali/JumboStringTest.java
@@ -15,7 +15,7 @@
 public class JumboStringTest extends SmaliTestBase {
 
   @Test
-  public void test() {
+  public void test() throws Exception {
     StringBuilder builder = new StringBuilder();
     StringBuilder expectedBuilder = new StringBuilder();
     builder.append("    new-instance         v0, Ljava/lang/StringBuilder;\n");
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index c726aad..c64e39c 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.code.ConstWide;
 import com.android.tools.r8.code.ConstWideHigh16;
 import com.android.tools.r8.code.DivInt;
+import com.android.tools.r8.code.DivInt2Addr;
 import com.android.tools.r8.code.InvokeStatic;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.MoveResult;
@@ -20,6 +21,7 @@
 import com.android.tools.r8.code.ReturnObject;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.code.ReturnWide;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -88,7 +90,7 @@
   }
 
   @Test
-  public void a() {
+  public void a() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     String returnType = "java.lang.String";
@@ -150,7 +152,7 @@
   }
 
   @Test
-  public void b() {
+  public void b() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     String returnType = "java.lang.String";
@@ -220,7 +222,7 @@
   }
 
   @Test
-  public void c() {
+  public void c() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // Method with const instructions after the outline.
@@ -277,7 +279,7 @@
   }
 
   @Test
-  public void d() {
+  public void d() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // Method with mixed use of arguments and locals.
@@ -416,7 +418,7 @@
   }
 
   @Test
-  public void doubleArguments() {
+  public void doubleArguments() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     String returnType = "java.lang.String";
@@ -490,7 +492,7 @@
   }
 
   @Test
-  public void invokeStatic() {
+  public void invokeStatic() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     String returnType = "void";
@@ -563,7 +565,7 @@
   }
 
   @Test
-  public void constructor() throws IOException, RecognitionException {
+  public void constructor() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature1 = builder.addStaticMethod(
@@ -648,7 +650,7 @@
   }
 
   @Test
-  public void constructorDontSplitNewInstanceAndInit() {
+  public void constructorDontSplitNewInstanceAndInit() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature = builder.addStaticMethod(
@@ -718,7 +720,7 @@
   }
 
   @Test
-  public void outlineWithoutArguments() {
+  public void outlineWithoutArguments() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature1 = builder.addStaticMethod(
@@ -763,7 +765,7 @@
   }
 
   @Test
-  public void outlineDifferentReturnType() {
+  public void outlineDifferentReturnType() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     List<String> parameters = Collections.singletonList("java.lang.StringBuilder");
@@ -850,7 +852,7 @@
   }
 
   @Test
-  public void outlineMultipleTimes() {
+  public void outlineMultipleTimes() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     String returnType = "java.lang.String";
@@ -923,7 +925,7 @@
   }
 
   @Test
-  public void outlineReturnLong() {
+  public void outlineReturnLong() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature = builder.addStaticMethod(
@@ -976,7 +978,7 @@
   }
 
   @Test
-  public void outlineArrayType() {
+  public void outlineArrayType() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticMethod(
@@ -1034,7 +1036,7 @@
   }
 
   @Test
-  public void outlineArithmeticBinop() {
+  public void outlineArithmeticBinop() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticMethod(
@@ -1115,7 +1117,7 @@
   }
 
   @Test
-  public void outlineWithHandler() {
+  public void outlineWithHandler() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticMethod(
@@ -1175,7 +1177,7 @@
     assertTrue(code.instructions[0] instanceof DivInt);
     assertTrue(code.instructions[1] instanceof InvokeStatic);
     assertTrue(code.instructions[2] instanceof MoveResult);
-    assertTrue(code.instructions[3] instanceof DivInt);
+    assertTrue(code.instructions[3] instanceof DivInt2Addr);
     assertTrue(code.instructions[4] instanceof Return);
     assertTrue(code.instructions[5] instanceof Const4);
     assertTrue(code.instructions[6] instanceof Return);
@@ -1188,7 +1190,7 @@
   }
 
   @Test
-  public void outlineUnusedOutValue() {
+  public void outlineUnusedOutValue() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // The result from neither the div-int is never used.
@@ -1235,7 +1237,7 @@
   }
 
   @Test
-  public void outlineUnusedNewInstanceOutValue() {
+  public void outlineUnusedNewInstanceOutValue() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // The result from the new-instance instructions are never used (<init> is not even called).
@@ -1280,7 +1282,7 @@
   }
 
   @Test
-  public void regress33733666() {
+  public void regress33733666() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addStaticMethod(
diff --git a/src/test/java/com/android/tools/r8/smali/Regress38014736.java b/src/test/java/com/android/tools/r8/smali/Regress38014736.java
index 91d8023..c97d412 100644
--- a/src/test/java/com/android/tools/r8/smali/Regress38014736.java
+++ b/src/test/java/com/android/tools/r8/smali/Regress38014736.java
@@ -12,7 +12,7 @@
 public class Regress38014736 extends SmaliTestBase {
 
   @Test
-  public void handlerRangeStartingOnMoveResult() {
+  public void handlerRangeStartingOnMoveResult() throws Exception {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     builder.addMainMethod(
diff --git a/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java b/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
index 45cd8cd..e1dc5c4 100644
--- a/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
+++ b/src/test/java/com/android/tools/r8/smali/RunArtSmokeTest.java
@@ -21,7 +21,7 @@
 public class RunArtSmokeTest extends SmaliTestBase {
 
   @Test
-  public void test() {
+  public void test() throws Exception {
     // Build simple "Hello, world!" application.
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
     MethodSignature mainSignature = builder.addMainMethod(
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 f35dd21..e2006d0 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.DexApplication;
@@ -387,7 +388,7 @@
       return iterator;
     }
 
-    public String run() {
+    public String run() throws DexOverflowException {
       AppInfo appInfo = new AppInfo(application);
       IRConverter converter = new IRConverter(application, appInfo, options);
       converter.replaceCodeForTesting(method, code);
@@ -514,11 +515,13 @@
         processdApplication, DEFAULT_CLASS_NAME, returnType, DEFAULT_METHOD_NAME, parameters);
   }
 
-  public String runArt(DexApplication application, InternalOptions options) {
+  public String runArt(DexApplication application, InternalOptions options)
+      throws DexOverflowException {
     return runArt(application, options, DEFAULT_MAIN_CLASS_NAME);
   }
 
-  public String runArt(DexApplication application, InternalOptions options, String mainClass) {
+  public String runArt(DexApplication application, InternalOptions options, String mainClass)
+      throws DexOverflowException {
     try {
       AndroidApp app = writeDex(application, options);
       Path out = temp.getRoot().toPath().resolve("run-art-input.zip");
@@ -530,7 +533,8 @@
     }
   }
 
-  public void runDex2Oat(DexApplication application, InternalOptions options) {
+  public void runDex2Oat(DexApplication application, InternalOptions options)
+      throws DexOverflowException {
     try {
       AndroidApp app = writeDex(application, options);
       Path dexOut = temp.getRoot().toPath().resolve("run-dex2oat-input.zip");
@@ -542,7 +546,8 @@
     }
   }
 
-  public AndroidApp writeDex(DexApplication application, InternalOptions options) {
+  public AndroidApp writeDex(DexApplication application, InternalOptions options)
+      throws DexOverflowException {
     AppInfo appInfo = new AppInfo(application);
     try {
       return R8.writeApplication(
diff --git a/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java b/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
index c256920..3750a86 100644
--- a/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/smali/SwitchRewritingTest.java
@@ -34,7 +34,8 @@
 public class SwitchRewritingTest extends SmaliTestBase {
 
   private boolean twoCaseWillUsePackedSwitch(int key1, int key2) {
-    return Math.abs((long) key1 - (long) key2) <= 2;
+    assert key1 != key2;
+    return Math.abs((long) key1 - (long) key2) == 1;
   }
 
   private boolean some16BitConst(Instruction instruction) {
@@ -114,7 +115,7 @@
     }
   }
 
-  private void runTwoCaseSparseToPackedDexTest(int key1, int key2) {
+  private void runTwoCaseSparseToPackedOrIfsDexTest(int key1, int key2) {
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     MethodSignature signature = builder.addStaticMethod(
@@ -156,24 +157,29 @@
     if (twoCaseWillUsePackedSwitch(key1, key2)) {
       assertTrue(code.instructions[0] instanceof PackedSwitch);
     } else {
-      assertTrue(code.instructions[0] instanceof SparseSwitch);
+      if (key1 == 0) {
+        assertTrue(code.instructions[0] instanceof IfEqz);
+      } else {
+        // Const instruction before if.
+        assertTrue(code.instructions[1] instanceof IfEq);
+      }
     }
   }
 
   @Test
-  public void twoCaseSparseToPackedDex() {
+  public void twoCaseSparseToPackedOrIfsDex() {
     for (int delta = 1; delta <= 3; delta++) {
-      runTwoCaseSparseToPackedDexTest(0, delta);
-      runTwoCaseSparseToPackedDexTest(-delta, 0);
-      runTwoCaseSparseToPackedDexTest(Integer.MIN_VALUE, Integer.MIN_VALUE + delta);
-      runTwoCaseSparseToPackedDexTest(Integer.MAX_VALUE - delta, Integer.MAX_VALUE);
+      runTwoCaseSparseToPackedOrIfsDexTest(0, delta);
+      runTwoCaseSparseToPackedOrIfsDexTest(-delta, 0);
+      runTwoCaseSparseToPackedOrIfsDexTest(Integer.MIN_VALUE, Integer.MIN_VALUE + delta);
+      runTwoCaseSparseToPackedOrIfsDexTest(Integer.MAX_VALUE - delta, Integer.MAX_VALUE);
     }
-    runTwoCaseSparseToPackedDexTest(-1, 1);
-    runTwoCaseSparseToPackedDexTest(-2, 1);
-    runTwoCaseSparseToPackedDexTest(-1, 2);
-    runTwoCaseSparseToPackedDexTest(Integer.MIN_VALUE, Integer.MAX_VALUE);
-    runTwoCaseSparseToPackedDexTest(Integer.MIN_VALUE + 1, Integer.MAX_VALUE);
-    runTwoCaseSparseToPackedDexTest(Integer.MIN_VALUE, Integer.MAX_VALUE - 1);
+    runTwoCaseSparseToPackedOrIfsDexTest(-1, 1);
+    runTwoCaseSparseToPackedOrIfsDexTest(-2, 1);
+    runTwoCaseSparseToPackedOrIfsDexTest(-1, 2);
+    runTwoCaseSparseToPackedOrIfsDexTest(Integer.MIN_VALUE, Integer.MAX_VALUE);
+    runTwoCaseSparseToPackedOrIfsDexTest(Integer.MIN_VALUE + 1, Integer.MAX_VALUE);
+    runTwoCaseSparseToPackedOrIfsDexTest(Integer.MIN_VALUE, Integer.MAX_VALUE - 1);
   }
 
   private void runLargerSwitchDexTest(int firstKey, int keyStep, int totalKeys,
@@ -356,7 +362,12 @@
     if (twoCaseWillUsePackedSwitch(key1, key2)) {
       assertTrue(code.instructions[3] instanceof PackedSwitch);
     } else {
-      assertTrue(code.instructions[3] instanceof SparseSwitch);
+      if (key1 == 0) {
+        assertTrue(code.instructions[3] instanceof IfEqz);
+      } else {
+        // Const instruction before if.
+        assertTrue(code.instructions[4] instanceof IfEq);
+      }
     }
   }
 
@@ -462,8 +473,8 @@
     runLargerSwitchJarTest(0, 1, 5503, null);
   }
 
-  private void runConvertCasesToIf(List<Integer> keys, int defaultValue, int expectedIfs)
-      throws Exception {
+  private void runConvertCasesToIf(List<Integer> keys, int defaultValue, int expectedIfs,
+      int expectedPackedSwitches, int expectedSparceSwitches) throws Exception {
     JasminBuilder builder = new JasminBuilder();
     JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
 
@@ -514,8 +525,8 @@
       }
     }
 
-    assertEquals(1, packedSwitches);
-    assertEquals(0, sparseSwitches);
+    assertEquals(expectedPackedSwitches, packedSwitches);
+    assertEquals(expectedSparceSwitches, sparseSwitches);
     assertEquals(expectedIfs, ifs);
 
     // Run the code
@@ -526,12 +537,34 @@
 
   @Test
   public void convertCasesToIf() throws Exception {
-    runConvertCasesToIf(ImmutableList.of(0, 1000, 1001, 1002, 1003, 1004), -100, 1);
-    runConvertCasesToIf(ImmutableList.of(1000, 1001, 1002, 1003, 1004, 2000), -100, 1);
-    runConvertCasesToIf(ImmutableList.of(Integer.MIN_VALUE, 1000, 1001, 1002, 1003, 1004), -100, 1);
-    runConvertCasesToIf(ImmutableList.of(1000, 1001, 1002, 1003, 1004, Integer.MAX_VALUE), -100, 1);
-    runConvertCasesToIf(ImmutableList.of(0, 1000, 1001, 1002, 1003, 1004, 2000), -100, 2);
+    // Switches that are completely converted to ifs.
+    runConvertCasesToIf(ImmutableList.of(0, 1000), -100, 2, 0, 0);
+    runConvertCasesToIf(ImmutableList.of(0, 1000, 2000), -100, 3, 0, 0);
+    runConvertCasesToIf(ImmutableList.of(0, 1000, 2000, 3000), -100, 4, 0, 0);
+
+    // Switches that are completely converted to ifs and one switch.
+    runConvertCasesToIf(ImmutableList.of(0, 1000, 1001, 1002, 1003, 1004), -100, 1, 1, 0);
+    runConvertCasesToIf(ImmutableList.of(1000, 1001, 1002, 1003, 1004, 2000), -100, 1, 1, 0);
     runConvertCasesToIf(ImmutableList.of(
-        Integer.MIN_VALUE, 1000, 1001, 1002, 1003, 1004, Integer.MAX_VALUE), -100, 2);
+        Integer.MIN_VALUE, 1000, 1001, 1002, 1003, 1004), -100, 1, 1, 0);
+    runConvertCasesToIf(ImmutableList.of(
+        1000, 1001, 1002, 1003, 1004, Integer.MAX_VALUE), -100, 1, 1, 0);
+    runConvertCasesToIf(ImmutableList.of(0, 1000, 1001, 1002, 1003, 1004, 2000), -100, 2, 1, 0);
+    runConvertCasesToIf(ImmutableList.of(
+        Integer.MIN_VALUE, 1000, 1001, 1002, 1003, 1004, Integer.MAX_VALUE), -100, 2, 1, 0);
+
+    // Switches that are completely converted to ifs and two switches.
+    runConvertCasesToIf(ImmutableList.of(
+        0, 1, 2, 3, 4, 1000, 1001, 1002, 1003, 1004), -100, 0, 2, 0);
+    runConvertCasesToIf(ImmutableList.of(
+        -1000, 0, 1, 2, 3, 4, 1000, 1001, 1002, 1003, 1004), -100, 1, 2, 0);
+    runConvertCasesToIf(ImmutableList.of(
+        -1000, 0, 1, 2, 3, 4, 1000, 1001, 1002, 1003, 1004, 2000), -100, 2, 2, 0);
+
+    // Switches that are completely converted two switches (one sparse and one packed).
+    runConvertCasesToIf(ImmutableList.of(
+        -1000, -900, -800, -700, -600, -500, -400, -300,
+        1000, 1001, 1002, 1003, 1004,
+        2000, 2100, 2200, 2300, 2400, 2500), -100, 0, 1, 1);
   }
 }
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 0ad240c..26de96c 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -56,6 +56,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNaming;
 import com.android.tools.r8.naming.MemberNaming;
@@ -691,8 +692,11 @@
   }
 
   public abstract class FieldSubject extends MemberSubject {
+    public abstract boolean hasStaticValue();
 
     public abstract DexEncodedField getField();
+
+    public abstract DexValue getStaticValue();
   }
 
   public class AbsentFieldSubject extends FieldSubject {
@@ -733,6 +737,16 @@
     }
 
     @Override
+    public boolean hasStaticValue() {
+      return false;
+    }
+
+    @Override
+    public DexValue getStaticValue() {
+      return null;
+    }
+
+    @Override
     public DexEncodedField getField() {
       return null;
     }
@@ -791,6 +805,16 @@
     }
 
     @Override
+    public boolean hasStaticValue() {
+      return dexField.staticValue != null;
+    }
+
+    @Override
+    public DexValue getStaticValue() {
+      return dexField.staticValue;
+    }
+
+    @Override
     public DexEncodedField getField() {
       return dexField;
     }
diff --git a/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
new file mode 100644
index 0000000..64e7a5b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/GenerateMainDexListCommandTest.java
@@ -0,0 +1,144 @@
+// 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.JAR_EXTENSION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.GenerateMainDexListCommand;
+import com.android.tools.r8.ToolHelper;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+public class GenerateMainDexListCommandTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Rule
+  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+  @Test
+  public void emptyCommand() throws Throwable {
+    verifyEmptyCommand(GenerateMainDexListCommand.builder().build());
+    verifyEmptyCommand(parse());
+    verifyEmptyCommand(parse(""));
+    verifyEmptyCommand(parse("", ""));
+    verifyEmptyCommand(parse(" "));
+    verifyEmptyCommand(parse(" ", " "));
+    verifyEmptyCommand(parse("\t"));
+    verifyEmptyCommand(parse("\t", "\t"));
+  }
+
+  private void verifyEmptyCommand(GenerateMainDexListCommand command) throws IOException {
+    assertEquals(0, ToolHelper.getApp(command).getDexProgramResources().size());
+    assertEquals(0, ToolHelper.getApp(command).getClassProgramResources().size());
+    assertFalse(ToolHelper.getApp(command).hasMainDexListResources());
+  }
+
+  // Add the jars used in the com.android.tools.r8.maindexlist.MainDexTracingTest test.
+  private void addInputJarsToCommandLine(List<String> args) {
+    args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidex001" + JAR_EXTENSION)
+        .toAbsolutePath().toString());
+    args.add(Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "multidexfakeframeworks" + JAR_EXTENSION)
+        .toAbsolutePath().toString());
+  }
+
+  // Add main-dex rules used in the com.android.tools.r8.maindexlist.MainDexTracingTest test.
+  private void addMainDexRuleToCommandLine(List<String> args) {
+    args.add("--main-dex-rules");
+    args.add(Paths.get(ToolHelper.EXAMPLES_DIR, "multidex", "main-dex-rules.txt")
+        .toAbsolutePath().toString());
+  }
+
+  @Test
+  public void defaultOutIsCwd() throws Throwable {
+    Path working = temp.getRoot().toPath();
+    String mainDexListOutput = "main-dex-list.txt";
+    Path output = working.resolve(mainDexListOutput);
+    assertFalse(Files.exists(output));
+    List<String> args = new ArrayList<>();
+    addInputJarsToCommandLine(args);
+    addMainDexRuleToCommandLine(args);
+    assertEquals(0, ToolHelper.forkGenerateMainDexList(
+        working, args, "--main-dex-list-output", mainDexListOutput).exitCode);
+    assertTrue(Files.exists(output));
+    assertTrue(Files.size(output) > 0);
+  }
+
+  @Test
+  public void validOutputPath() throws Throwable {
+    Path existingFile = temp.getRoot().toPath().resolve("existing_output");
+    try (OutputStream existingFileOut = Files.newOutputStream(existingFile,
+        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
+      PrintWriter writer = new PrintWriter(existingFileOut);
+      writer.println("Hello, world!");
+      writer.flush();
+    }
+    Path nonExistingFile = temp.getRoot().toPath().resolve("non_existing_output");
+    assertEquals(
+        existingFile,
+        GenerateMainDexListCommand.builder().setMainDexListOutputPath(existingFile).build()
+            .getMainDexListOutputPath());
+    assertEquals(
+        nonExistingFile,
+        GenerateMainDexListCommand.builder().setMainDexListOutputPath(nonExistingFile).build()
+            .getMainDexListOutputPath());
+    assertEquals(
+        existingFile,
+        parse("--main-dex-list-output", existingFile.toString()).getMainDexListOutputPath());
+    assertEquals(
+        nonExistingFile,
+        parse("--main-dex-list-output", nonExistingFile.toString()).getMainDexListOutputPath());
+  }
+
+  @Test
+  public void nonExistingOutputFileInNonExistingDir() throws Throwable {
+    Path nonExistingFileInNonExistingDir =
+        temp.getRoot().toPath().resolve("a/path/that/does/not/exist");
+    assertEquals(
+        nonExistingFileInNonExistingDir,
+        GenerateMainDexListCommand.builder()
+            .setMainDexListOutputPath(nonExistingFileInNonExistingDir).build()
+            .getMainDexListOutputPath());
+    assertEquals(
+        nonExistingFileInNonExistingDir,
+        parse("--main-dex-list-output",
+            nonExistingFileInNonExistingDir.toString()).getMainDexListOutputPath());
+  }
+
+  @Test
+  public void mainDexRules() throws Throwable {
+    Path mainDexRules1 = temp.newFile("main-dex-1.rules").toPath();
+    Path mainDexRules2 = temp.newFile("main-dex-2.rules").toPath();
+    parse("--main-dex-rules", mainDexRules1.toString());
+    parse(
+        "--main-dex-rules", mainDexRules1.toString(), "--main-dex-rules", mainDexRules2.toString());
+  }
+
+  @Test
+  public void mainDexList() throws Throwable {
+    Path mainDexList1 = temp.newFile("main-dex-list-1.txt").toPath();
+    Path mainDexList2 = temp.newFile("main-dex-list-2.txt").toPath();
+    parse("--main-dex-list", mainDexList1.toString());
+    parse("--main-dex-list", mainDexList1.toString(), "--main-dex-list", mainDexList2.toString());
+  }
+
+  private GenerateMainDexListCommand parse(String... args) throws Throwable {
+    return GenerateMainDexListCommand.parse(args).build();
+  }
+}
diff --git a/third_party/kotlin.tar.gz.sha1 b/third_party/kotlin.tar.gz.sha1
index 942c691..f9d8e24 100644
--- a/third_party/kotlin.tar.gz.sha1
+++ b/third_party/kotlin.tar.gz.sha1
@@ -1 +1 @@
-21b6244cb0a5bca1f1d046d00540a610c02cd714
\ No newline at end of file
+28f3708659a1daa879bc8c707da54e3617a2070f
\ No newline at end of file
diff --git a/tools/archive.py b/tools/archive.py
new file mode 100755
index 0000000..ee1053d
--- /dev/null
+++ b/tools/archive.py
@@ -0,0 +1,47 @@
+#!/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 d8
+import os
+import r8
+import sys
+import utils
+
+ARCHIVE_BUCKET = 'r8-releases'
+
+def GetVersion():
+  r8_version = r8.run(['--version'], build = False).strip()
+  d8_version = d8.run(['--version'], build = False).strip()
+  # The version printed is "D8 vVERSION_NUMBER" and "R8 vVERSION_NUMBER"
+  # Sanity check that versions match.
+  if d8_version.split()[1] != r8_version.split()[1]:
+    raise Exception(
+        'Version mismatch: \n%s\n%s' % (d8_version, r8_version))
+  return d8_version.split()[1]
+
+def GetStorageDestination(storage_prefix, version, file_name):
+  return '%s%s/raw/%s/%s' % (storage_prefix, ARCHIVE_BUCKET, version, file_name)
+
+def GetUploadDestination(version, file_name):
+  return GetStorageDestination('gs://', version, file_name)
+
+def GetUrl(version, file_name):
+  return GetStorageDestination('http://storage.googleapis.com/',
+                               version,
+                               file_name)
+
+def Main():
+  if not 'BUILDBOT_BUILDERNAME' in os.environ:
+    raise Exception('You are not a bot, don\'t archive builds')
+  version = GetVersion()
+  for jar in [utils.D8_JAR, utils.R8_JAR]:
+    file_name = os.path.basename(jar)
+    destination = GetUploadDestination(version, file_name)
+    print('Uploading %s to %s' % (jar, destination))
+    utils.upload_file_to_cloud_storage(jar, destination)
+    print('File available at: %s' % GetUrl(version, file_name))
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/d8.py b/tools/d8.py
index d214cf5..5478897 100755
--- a/tools/d8.py
+++ b/tools/d8.py
@@ -9,9 +9,8 @@
 import sys
 import utils
 
-D8_JAR = os.path.join(utils.REPO_ROOT, 'build', 'libs', 'd8.jar')
-
-def run(args, build = True, debug = True, profile = False, track_memory_file=None):
+def run(args, build = True, debug = True, profile = False,
+        track_memory_file=None):
   if build:
     gradle.RunGradle(['D8'])
   cmd = []
@@ -22,10 +21,12 @@
     cmd.append('-ea')
   if profile:
     cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
-  cmd.extend(['-jar', D8_JAR])
+  cmd.extend(['-jar', utils.D8_JAR])
   cmd.extend(args)
   utils.PrintCmd(cmd)
-  subprocess.check_call(cmd)
+  result = subprocess.check_output(cmd)
+  print(result)
+  return result
 
 def main():
   build = True
diff --git a/tools/r8.py b/tools/r8.py
index b453227e..7de2c94 100755
--- a/tools/r8.py
+++ b/tools/r8.py
@@ -9,9 +9,8 @@
 import sys
 import utils
 
-R8_JAR = os.path.join(utils.REPO_ROOT, 'build', 'libs', 'r8.jar')
-
-def run(args, build = True, debug = True, profile = False, track_memory_file=None):
+def run(args, build = True, debug = True, profile = False,
+        track_memory_file=None):
   if build:
     gradle.RunGradle(['r8'])
   cmd = []
@@ -22,10 +21,12 @@
     cmd.append('-ea')
   if profile:
     cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
-  cmd.extend(['-jar', R8_JAR])
+  cmd.extend(['-jar', utils.R8_JAR])
   cmd.extend(args)
   utils.PrintCmd(cmd)
-  subprocess.check_call(cmd)
+  result = subprocess.check_output(cmd)
+  print(result)
+  return result
 
 def main():
   build = True
diff --git a/tools/test.py b/tools/test.py
index 2b68c88..c0a1e02 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -77,7 +77,7 @@
   upload_dir = os.path.join(utils.REPO_ROOT, 'build', 'reports', 'tests')
   u_dir = uuid.uuid4()
   destination = 'gs://%s/%s' % (BUCKET, u_dir)
-  utils.upload_html_to_cloud_storage(upload_dir, destination)
+  utils.upload_dir_to_cloud_storage(upload_dir, destination)
   url = 'http://storage.googleapis.com/%s/%s/test/index.html' % (BUCKET, u_dir)
   print 'Test results available at: %s' % url
   print '@@@STEP_LINK@Test failures@%s@@@' % url
diff --git a/tools/test_android_cts.py b/tools/test_android_cts.py
index 3f66302..887e3ae 100755
--- a/tools/test_android_cts.py
+++ b/tools/test_android_cts.py
@@ -39,7 +39,6 @@
   'aosp_manifest.xml')
 AOSP_HELPER_SH = join(utils.REPO_ROOT, 'scripts', 'aosp_helper.sh')
 
-D8_JAR = join(utils.REPO_ROOT, 'build/libs/d8.jar')
 D8LOGGER_JAR = join(utils.REPO_ROOT, 'build/libs/d8logger.jar')
 
 AOSP_ROOT = join(utils.REPO_ROOT, 'build/aosp')
@@ -133,7 +132,7 @@
   counter = 0
   if tool_is_d8 or clean_dex:
     if not clean_dex:
-      d8jar_mtime = os.path.getmtime(D8_JAR)
+      d8jar_mtime = os.path.getmtime(utils.D8_JAR)
     dex_files = (chain.from_iterable(glob(join(x[0], '*.dex'))
       for x in os.walk(OUT_IMG)))
     for f in dex_files:
diff --git a/tools/test_framework.py b/tools/test_framework.py
index 9e5f63f..51c870a 100755
--- a/tools/test_framework.py
+++ b/tools/test_framework.py
@@ -30,7 +30,6 @@
 
 DX_JAR = os.path.join(utils.REPO_ROOT, 'tools', 'linux', 'dx', 'framework',
     'dx.jar')
-D8_JAR = os.path.join(utils.REPO_ROOT, 'build', 'libs', 'd8.jar')
 GOYT_EXE = os.path.join('third_party', 'goyt',
     'goyt_164843480')
 FRAMEWORK_JAR = os.path.join('third_party', 'framework',
@@ -57,16 +56,22 @@
              ' peak resident set size (VmHWM) in bytes.',
       default = False,
       action = 'store_true')
+  parser.add_argument('--output',
+                      help = 'Output directory to keep the generated files')
   return parser.parse_args()
 
 def Main():
   utils.check_java_version()
   args = parse_arguments()
+  output_dir = args.output
 
   with utils.TempDir() as temp_dir:
 
+    if not output_dir:
+      output_dir = temp_dir
+
     if args.tool in ['dx', 'goyt', 'goyt-release']:
-      tool_args = ['--dex', '--output=' + temp_dir, '--multi-dex',
+      tool_args = ['--dex', '--output=' + output_dir, '--multi-dex',
           '--min-sdk-version=' + MIN_SDK_VERSION]
 
     xmx = None
@@ -79,8 +84,8 @@
       tool_file = DX_JAR
       xmx = '-Xmx1600m'
     else:
-      tool_file = D8_JAR
-      tool_args = ['--output', temp_dir, '--min-api', MIN_SDK_VERSION]
+      tool_file = utils.D8_JAR
+      tool_args = ['--output', output_dir, '--min-api', MIN_SDK_VERSION]
       if args.tool == 'd8-release':
         tool_args.append('--release')
       xmx = '-Xmx600m'
@@ -89,7 +94,7 @@
 
     track_memory_file = None
     if args.print_memoryuse:
-      track_memory_file = os.path.join(temp_dir, utils.MEMORY_USE_TMP_FILE)
+      track_memory_file = os.path.join(output_dir, utils.MEMORY_USE_TMP_FILE)
       cmd.extend(['tools/track_memory.sh', track_memory_file])
 
     if tool_file.endswith('.jar'):
@@ -108,7 +113,7 @@
       print('{}-Total(MemoryUse): {}'
           .format(args.name, utils.grep_memoryuse(track_memory_file)))
 
-    dex_files = [f for f in glob(os.path.join(temp_dir, '*.dex'))]
+    dex_files = [f for f in glob(os.path.join(output_dir, '*.dex'))]
     code_size = 0
     for dex_file in dex_files:
       code_size += os.path.getsize(dex_file)
diff --git a/tools/utils.py b/tools/utils.py
index e281ccd..44e608d 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -19,6 +19,8 @@
     'dexsegments.jar')
 DEX_SEGMENTS_RESULT_PATTERN = re.compile('- ([^:]+): ([0-9]+)')
 COMPATDX_JAR = os.path.join(REPO_ROOT, 'build', 'libs', 'compatdx.jar')
+D8_JAR = os.path.join(REPO_ROOT, 'build', 'libs', 'd8.jar')
+R8_JAR = os.path.join(REPO_ROOT, 'build', 'libs', 'r8.jar')
 
 def PrintCmd(s):
   if type(s) is list:
@@ -55,13 +57,18 @@
     if not os.path.isdir(path):
         raise
 
-def upload_html_to_cloud_storage(directory, destination):
+def upload_dir_to_cloud_storage(directory, destination):
   # Upload and make the content encoding right for viewing directly
   cmd = ['gsutil.py', 'cp', '-z', 'html', '-a',
          'public-read', '-R', directory, destination]
   PrintCmd(cmd)
   subprocess.check_call(cmd)
 
+def upload_file_to_cloud_storage(source, destination):
+  cmd = ['gsutil.py', 'cp', '-a', 'public-read', source, destination]
+  PrintCmd(cmd)
+  subprocess.check_call(cmd)
+
 class TempDir(object):
  def __init__(self, prefix=''):
    self._temp_dir = None