Merge "Add an option for output of the main dex list"
diff --git a/build.gradle b/build.gradle
index 0a512f2..967923c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,8 @@
 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-import org.gradle.internal.os.OperatingSystem;
+import org.gradle.internal.os.OperatingSystem
+import utils.Utils;
 
 apply plugin: 'java'
 apply plugin: 'idea'
@@ -984,7 +985,7 @@
     def artTestDir = file("${androidCheckoutDir}/art/test")
     def artRunTestScript = file("${artTestDir}/run-test")
     def dxExecutable = new File("tools/linux/dx/bin/dx");
-    def dexMergerExecutable = new File("tools/linux/dx/bin/dexmerger");
+    def dexMergerExecutable = Utils.dexMergerExecutable()
     def dexToolName = dexTool == DexTool.DX ? "dx" : "jack"
 
     def name = dir.getName();
diff --git a/buildSrc/src/main/java/dx/DexMerger.java b/buildSrc/src/main/java/dx/DexMerger.java
index 39a4bfb..d3d8427 100644
--- a/buildSrc/src/main/java/dx/DexMerger.java
+++ b/buildSrc/src/main/java/dx/DexMerger.java
@@ -53,7 +53,7 @@
       public void execute(ExecSpec execSpec) {
         try {
           if (dexMergerExecutable == null) {
-            dexMergerExecutable = new File("tools/" + Utils.toolsDir() + "/dx/bin/dexmerger");
+            dexMergerExecutable = Utils.dexMergerExecutable();
           }
           execSpec.setExecutable(dexMergerExecutable);
           execSpec.args(destination.getCanonicalPath());
diff --git a/buildSrc/src/main/java/utils/Utils.java b/buildSrc/src/main/java/utils/Utils.java
index 4cbc90c..7158e80 100644
--- a/buildSrc/src/main/java/utils/Utils.java
+++ b/buildSrc/src/main/java/utils/Utils.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package utils;
 
+import java.io.File;
+
 public class Utils {
   public static String toolsDir() {
     String osName = System.getProperty("os.name");
@@ -14,4 +16,9 @@
       return "linux";
     }
   }
-}
+
+  public static File dexMergerExecutable() {
+    String executableName = Utils.toolsDir().equals("windows") ? "dexmerger.bat" : "dexmerger";
+    return new File("tools/" + Utils.toolsDir() + "/dx/bin/" + executableName);
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/ResourceProvider.java b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
similarity index 73%
rename from src/main/java/com/android/tools/r8/ResourceProvider.java
rename to src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
index bed36e3..7dbb420 100644
--- a/src/main/java/com/android/tools/r8/ResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ClassFileResourceProvider.java
@@ -3,17 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import java.util.Set;
+
 /**
- * Represents a provider for application resources. All resources returned
- * via this provider should be class file resources, other resource kinds
- * are not yet supported.
+ * Represents a provider for application resources of class file kind.
  *
  * Note that the classes will only be created for resources provided by
  * resource providers on-demand when they are needed by the tool. If
  * never needed, the resource will never be loaded.
  */
-public interface ResourceProvider {
-  // TODO: Consider adding support for DEX resources.
+public interface ClassFileResourceProvider {
+  /** Returns all class descriptors. */
+  Set<String> getClassDescriptors();
 
   /**
    * Get the class resource associated with the descriptor, or null if
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index ceefac5..c06438f 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OffOrAuto;
 import com.android.tools.r8.utils.OutputMode;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -53,7 +53,7 @@
     public Builder addClasspathFiles(Collection<Path> files) throws IOException {
       for (Path file : files) {
         if (isArchive(file)) {
-          addClasspathResourceProvider(PreloadedResourceProvider.fromArchive(file));
+          addClasspathResourceProvider(PreloadedClassFileProvider.fromArchive(file));
         } else {
           super.addClasspathFiles(file);
         }
@@ -70,7 +70,7 @@
     public Builder addLibraryFiles(Collection<Path> files) throws IOException {
       for (Path file : files) {
         if (isArchive(file)) {
-          addLibraryResourceProvider(PreloadedResourceProvider.fromArchive(file));
+          addLibraryResourceProvider(PreloadedClassFileProvider.fromArchive(file));
         } else {
           super.addLibraryFiles(file);
         }
@@ -78,12 +78,12 @@
       return this;
     }
 
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addClasspathResourceProvider(provider);
       return this;
     }
 
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       getAppBuilder().addLibraryResourceProvider(provider);
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 8b57155..7dd2d33 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -39,7 +39,6 @@
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.AttributeRemovalOptions;
-import com.android.tools.r8.utils.OutputMode;
 import com.android.tools.r8.utils.PackageDistribution;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -375,12 +374,12 @@
    */
   public static AndroidApp run(R8Command command)
       throws IOException, CompilationException, ProguardRuleParserException {
-    InternalOptions options = command.getInternalOptions();
-    AndroidApp outputApp =
-        runForTesting(command.getInputApp(), options).androidApp;
-    writeOutputs(command, options, outputApp);
-
-    return outputApp;
+    ExecutorService executorService = ThreadUtils.getExecutorService(command.getInternalOptions());
+    try {
+      return run(command, executorService);
+    } finally {
+      executorService.shutdown();
+    }
   }
 
   static void writeOutputs(R8Command command, InternalOptions options, AndroidApp outputApp)
@@ -445,11 +444,10 @@
    */
   public static AndroidApp run(R8Command command, ExecutorService executor)
       throws IOException, CompilationException, ProguardRuleParserException {
+    InternalOptions options = command.getInternalOptions();
     AndroidApp outputApp =
-        runForTesting(command.getInputApp(), command.getInternalOptions(), executor).androidApp;
-    if (command.getOutputPath() != null) {
-      outputApp.write(command.getOutputPath(), OutputMode.Indexed);
-    }
+        runForTesting(command.getInputApp(), options, executor).androidApp;
+    writeOutputs(command, options, outputApp);
     return outputApp;
   }
 
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index 183e513..148c167 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -8,8 +8,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexApplication.Builder;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.NamingLens;
@@ -24,7 +22,6 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -261,23 +258,20 @@
     System.out.println("Next bisection range: " + nextRange);
     int goodClasses = 0;
     int badClasses = 0;
-    Map<DexType, DexClass> classes = new HashMap<>();
-    for (DexLibraryClass clazz : badApp.libraryClasses()) {
-      classes.put(clazz.type, clazz);
-    }
+    List<DexProgramClass> programClasses = new ArrayList<>();
     for (DexProgramClass clazz : badApp.classes()) {
       DexProgramClass goodClass = getGoodClass(clazz);
       if (goodClass != null) {
-        classes.put(goodClass.type, goodClass);
+        programClasses.add(goodClass);
         ++goodClasses;
       } else {
-        classes.put(clazz.type, clazz);
+        programClasses.add(clazz);
         assert !nextRange.isEmpty();
         ++badClasses;
       }
     }
     System.out.println("Class split is good: " + goodClasses + ", bad: " + badClasses);
-    return new Builder(badApp, classes).build();
+    return new Builder(badApp).replaceProgramClasses(programClasses).build();
   }
 
   private DexProgramClass getGoodClass(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index e7bf9db..c7a66c2 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -8,20 +8,29 @@
 import static com.android.tools.r8.dex.Constants.ANDROID_O_API;
 import static com.android.tools.r8.dex.Constants.ANDROID_O_DEX_VERSION;
 import static com.android.tools.r8.dex.Constants.DEFAULT_ANDROID_API;
+import static com.android.tools.r8.graph.ClassKind.CLASSPATH;
+import static com.android.tools.r8.graph.ClassKind.LIBRARY;
+import static com.android.tools.r8.graph.ClassKind.PROGRAM;
 import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.graph.JarClassFileReader;
 import com.android.tools.r8.naming.ProguardMapReader;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ClassProvider;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
 import com.android.tools.r8.utils.MainDexList;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -30,6 +39,8 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -64,82 +75,25 @@
     final DexApplication.Builder builder = new DexApplication.Builder(itemFactory, timing);
     try (Closer closer = Closer.create()) {
       List<Future<?>> futures = new ArrayList<>();
+      // Still preload some of the classes, primarily for two reasons:
+      // (a) class lazy loading is not supported for DEX files
+      //     now and current implementation of parallel DEX file
+      //     loading will be lost with on-demand class loading.
+      // (b) some of the class file resources don't provide information
+      //     about class descriptor.
+      // TODO: try and preload less classes.
       readProguardMap(builder, executorService, futures, closer);
       readMainDexList(builder, executorService, futures, closer);
-      readDexSources(builder, executorService, futures, closer);
-      readClassSources(builder, closer);
-      initializeLazyClassCollection(builder);
+      ClassReader classReader = new ClassReader(executorService, futures, closer);
+      classReader.readSources();
       ThreadUtils.awaitFutures(futures);
+      classReader.initializeLazyClassCollection(builder);
     } finally {
       timing.end();
     }
     return builder.build();
   }
 
-  private void readClassSources(DexApplication.Builder builder, Closer closer)
-      throws IOException, ExecutionException {
-    JarApplicationReader application = new JarApplicationReader(options);
-    JarClassFileReader reader = new JarClassFileReader(
-        application, builder::addClassIgnoringLibraryDuplicates);
-    for (Resource input : inputApp.getClassProgramResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.PROGRAM, input.getStream(closer));
-    }
-    for (Resource input : inputApp.getClassClasspathResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, input.getStream(closer));
-    }
-    for (Resource input : inputApp.getClassLibraryResources()) {
-      reader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, input.getStream(closer));
-    }
-  }
-
-  private void initializeLazyClassCollection(DexApplication.Builder builder) {
-    List<ResourceProvider> classpathProviders = inputApp.getClasspathResourceProviders();
-    List<ResourceProvider> libraryProviders = inputApp.getLibraryResourceProviders();
-    if (!classpathProviders.isEmpty() || !libraryProviders.isEmpty()) {
-      builder.setLazyClassCollection(new LazyClassCollection(
-          new JarApplicationReader(options), classpathProviders, libraryProviders));
-    }
-  }
-
-  private void readDexSources(DexApplication.Builder builder, ExecutorService executorService,
-      List<Future<?>> futures, Closer closer)
-      throws IOException, ExecutionException {
-    List<Resource> dexProgramSources = inputApp.getDexProgramResources();
-    List<Resource> dexClasspathSources = inputApp.getDexClasspathResources();
-    List<Resource> dexLibrarySources = inputApp.getDexLibraryResources();
-    int numberOfFiles = dexProgramSources.size()
-        + dexLibrarySources.size() + dexClasspathSources.size();
-    if (numberOfFiles > 0) {
-      List<DexFileReader> fileReaders = new ArrayList<>(numberOfFiles);
-      int computedMinApiLevel = options.minApiLevel;
-      for (Resource input : dexProgramSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.PROGRAM, itemFactory));
-      }
-      for (Resource input : dexClasspathSources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        fileReaders.add(new DexFileReader(file, ClassKind.CLASSPATH, itemFactory));
-      }
-      for (Resource input : dexLibrarySources) {
-        DexFile file = new DexFile(input.getStream(closer));
-        computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
-        fileReaders.add(new DexFileReader(file, ClassKind.LIBRARY, itemFactory));
-      }
-      options.minApiLevel = computedMinApiLevel;
-      for (DexFileReader reader : fileReaders) {
-        DexFileReader.populateIndexTables(reader);
-      }
-      // Read the DexCode items and DexProgramClass items in parallel.
-      for (DexFileReader reader : fileReaders) {
-        futures.add(executorService.submit(() -> {
-          reader.addCodeItemsTo();  // Depends on Everything for parsing.
-          reader.addClassDefsTo(builder::addClass);  // Depends on Methods, Code items etc.
-        }));
-      }
-    }
-  }
-
   private int verifyOrComputeMinApiLevel(int computedMinApiLevel, DexFile file) {
     int version = file.getDexVersion();
     if (options.minApiLevel == DEFAULT_ANDROID_API) {
@@ -203,4 +157,111 @@
       }));
     }
   }
+
+  private final class ClassReader {
+    private final ExecutorService executorService;
+    private final List<Future<?>> futures;
+    private final Closer closer;
+
+    // We use concurrent queues to collect classes
+    // since the classes can be collected concurrently.
+    private final Queue<DexProgramClass> programClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexClasspathClass> classpathClasses = new ConcurrentLinkedQueue<>();
+    private final Queue<DexLibraryClass> libraryClasses = new ConcurrentLinkedQueue<>();
+    // Jar application reader to share across all class readers.
+    private final JarApplicationReader application = new JarApplicationReader(options);
+
+    ClassReader(ExecutorService executorService, List<Future<?>> futures, Closer closer) {
+      this.executorService = executorService;
+      this.futures = futures;
+      this.closer = closer;
+    }
+
+    private <T extends DexClass> void readDexSources(List<Resource> dexSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      if (dexSources.size() > 0) {
+        List<DexFileReader> fileReaders = new ArrayList<>(dexSources.size());
+        int computedMinApiLevel = options.minApiLevel;
+        for (Resource input : dexSources) {
+          DexFile file = new DexFile(input.getStream(closer));
+          computedMinApiLevel = verifyOrComputeMinApiLevel(computedMinApiLevel, file);
+          fileReaders.add(new DexFileReader(file, classKind, itemFactory));
+        }
+        options.minApiLevel = computedMinApiLevel;
+        for (DexFileReader reader : fileReaders) {
+          DexFileReader.populateIndexTables(reader);
+        }
+        // Read the DexCode items and DexProgramClass items in parallel.
+        for (DexFileReader reader : fileReaders) {
+          futures.add(executorService.submit(() -> {
+            reader.addCodeItemsTo();  // Depends on Everything for parsing.
+            reader.addClassDefsTo(
+                classKind.bridgeConsumer(classes::add)); // Depends on Methods, Code items etc.
+          }));
+        }
+      }
+    }
+
+    private <T extends DexClass> void readClassSources(List<Resource> classSources,
+        ClassKind classKind, Queue<T> classes) throws IOException, ExecutionException {
+      JarClassFileReader reader = new JarClassFileReader(
+          application, classKind.bridgeConsumer(classes::add));
+      for (Resource input : classSources) {
+        reader.read(DEFAULT_DEX_FILENAME, classKind, input.getStream(closer));
+      }
+    }
+
+    void readSources() throws IOException, ExecutionException {
+      readDexSources(inputApp.getDexProgramResources(), PROGRAM, programClasses);
+      readDexSources(inputApp.getDexClasspathResources(), CLASSPATH, classpathClasses);
+      readDexSources(inputApp.getDexLibraryResources(), LIBRARY, libraryClasses);
+      readClassSources(inputApp.getClassProgramResources(), PROGRAM, programClasses);
+      readClassSources(inputApp.getClassClasspathResources(), CLASSPATH, classpathClasses);
+      readClassSources(inputApp.getClassLibraryResources(), LIBRARY, libraryClasses);
+    }
+
+    private <T extends DexClass> ClassProvider<T> buildClassProvider(ClassKind classKind,
+        Queue<T> preloadedClasses, List<ClassFileResourceProvider> resourceProviders,
+        JarApplicationReader reader) {
+      List<ClassProvider<T>> providers = new ArrayList<>();
+
+      // Preloaded classes.
+      if (!preloadedClasses.isEmpty()) {
+        providers.add(ClassProvider.forPreloadedClasses(classKind, preloadedClasses));
+      }
+
+      // Class file resource providers.
+      for (ClassFileResourceProvider provider : resourceProviders) {
+        providers.add(ClassProvider.forClassFileResources(classKind, provider, reader));
+      }
+
+      // Combine if needed.
+      if (providers.isEmpty()) {
+        return null;
+      }
+      return providers.size() == 1 ? providers.get(0)
+          : ClassProvider.combine(classKind, providers);
+    }
+
+    void initializeLazyClassCollection(DexApplication.Builder builder) {
+      // Add all program classes to the builder.
+      for (DexProgramClass clazz : programClasses) {
+        builder.addProgramClass(clazz.asProgramClass());
+      }
+
+      // Create classpath class collection if needed.
+      ClassProvider<DexClasspathClass> classpathClassProvider = buildClassProvider(CLASSPATH,
+          classpathClasses, inputApp.getClasspathResourceProviders(), application);
+      if (classpathClassProvider != null) {
+        builder.setClasspathClassCollection(new ClasspathClassCollection(classpathClassProvider));
+      }
+
+      // Create library class collection if needed.
+      ClassProvider<DexLibraryClass> libraryClassProvider = buildClassProvider(LIBRARY,
+          libraryClasses, inputApp.getLibraryResourceProviders(), application);
+      if (libraryClassProvider != null) {
+        builder.setLibraryClassCollection(new LibraryClassCollection(libraryClassProvider));
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 73c0533..9e3e118 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -21,7 +21,7 @@
 
   public AppInfoWithSubtyping(DexApplication application) {
     super(application);
-    populateSubtypeMap(application.getClassMap(), application.dexItemFactory);
+    populateSubtypeMap(application.getFullClassMap(), application.dexItemFactory);
   }
 
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous) {
@@ -33,7 +33,7 @@
   protected AppInfoWithSubtyping(AppInfoWithSubtyping previous, GraphLense lense) {
     super(previous, lense);
     // Recompute subtype map if we have modified the graph.
-    populateSubtypeMap(previous.app.getClassMap(), dexItemFactory);
+    populateSubtypeMap(previous.app.getFullClassMap(), dexItemFactory);
   }
 
   public Set<DexType> getMissingClasses() {
diff --git a/src/main/java/com/android/tools/r8/graph/ClassKind.java b/src/main/java/com/android/tools/r8/graph/ClassKind.java
index 6f53a40..9298ba5 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassKind.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassKind.java
@@ -1,12 +1,14 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.Resource;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Kind of the application class. Can be program, classpath or library. */
 public enum ClassKind {
-  PROGRAM(DexProgramClass::new),
-  CLASSPATH(DexClasspathClass::new),
-  LIBRARY(DexLibraryClass::new);
+  PROGRAM(DexProgramClass::new, DexClass::isProgramClass),
+  CLASSPATH(DexClasspathClass::new, DexClass::isClasspathClass),
+  LIBRARY(DexLibraryClass::new, DexClass::isLibraryClass);
 
   private interface Factory {
     DexClass create(DexType type, Resource.Kind origin, DexAccessFlags accessFlags,
@@ -17,9 +19,11 @@
   }
 
   private final Factory factory;
+  private final Predicate<DexClass> check;
 
-  ClassKind(Factory factory) {
+  ClassKind(Factory factory, Predicate<DexClass> check) {
     this.factory = factory;
+    this.check = check;
   }
 
   public DexClass create(
@@ -30,4 +34,16 @@
     return factory.create(type, origin, accessFlags, superType, interfaces, sourceFile,
         annotations, staticFields, instanceFields, directMethods, virtualMethods);
   }
+
+  public boolean isOfKind(DexClass clazz) {
+    return check.test(clazz);
+  }
+
+  public <T extends DexClass> Consumer<DexClass> bridgeConsumer(Consumer<T> consumer) {
+    return clazz -> {
+      assert isOfKind(clazz);
+      @SuppressWarnings("unchecked") T specialized = (T) clazz;
+      consumer.accept(specialized);
+    };
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index af586db..e7485aa 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -6,16 +6,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.ir.desugar.LambdaRewriter;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.ClasspathClassCollection;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.LazyClassCollection;
+import com.android.tools.r8.utils.LibraryClassCollection;
+import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.io.ByteArrayOutputStream;
@@ -26,8 +23,10 @@
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Hashtable;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -35,16 +34,9 @@
 public class DexApplication {
 
   // Maps type into class, may be used concurrently.
-  private final ImmutableMap<DexType, DexClass> classMap;
-
-  // Lazily loaded classes.
-  //
-  // Note that this collection is autonomous and may be used in several
-  // different applications. Particularly, it is the case when one
-  // application is being build based on another one. Among others,
-  // it will have an important side-effect: class conflict resolution,
-  // generated errors in particular, may be different in lazy scenario.
-  private final LazyClassCollection lazyClassCollection;
+  private ProgramClassCollection programClasses;
+  private ClasspathClassCollection classpathClasses;
+  private LibraryClassCollection libraryClasses;
 
   public final ImmutableSet<DexType> mainDexList;
 
@@ -60,67 +52,97 @@
   /** Constructor should only be invoked by the DexApplication.Builder. */
   private DexApplication(
       ClassNameMapper proguardMap,
-      ImmutableMap<DexType, DexClass> classMap,
-      LazyClassCollection lazyClassCollection,
+      ProgramClassCollection programClasses,
+      ClasspathClassCollection classpathClasses,
+      LibraryClassCollection libraryClasses,
       ImmutableSet<DexType> mainDexList,
       DexItemFactory dexItemFactory,
       DexString highestSortingString,
       Timing timing) {
+    assert programClasses != null;
     this.proguardMap = proguardMap;
-    this.lazyClassCollection = lazyClassCollection;
+    this.programClasses = programClasses;
+    this.classpathClasses = classpathClasses;
+    this.libraryClasses = libraryClasses;
     this.mainDexList = mainDexList;
-    this.classMap = classMap;
     this.dexItemFactory = dexItemFactory;
     this.highestSortingString = highestSortingString;
     this.timing = timing;
   }
 
-  ImmutableMap<DexType, DexClass> getClassMap() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    return classMap;
+  /** Force load all classes and return type -> class map containing all the classes */
+  public Map<DexType, DexClass> getFullClassMap() {
+    return forceLoadAllClasses();
   }
 
-  public Iterable<DexProgramClass> classes() {
-    List<DexProgramClass> result = new ArrayList<>();
-    // Note: we ignore lazy class collection because it
-    // is never supposed to be used for program classes.
-    for (DexClass clazz : classMap.values()) {
-      if (clazz.isProgramClass()) {
-        result.add(clazz.asProgramClass());
-      }
-    }
-    return result;
+  // Reorder classes randomly. Note that the order of classes in program or library
+  // class collections should not matter for compilation of valid code and when running
+  // with assertions enabled we reorder the classes randomly to catch possible issues.
+  // Also note that the order may add to non-determinism in reporting errors for invalid
+  // code, but this non-determinism exists even with the same order of classes since we
+  // may process classes concurrently and fail-fast on the first error.
+  private <T> boolean reorderClasses(List<T> classes) {
+    Collections.shuffle(classes);
+    return true;
   }
 
-  public Iterable<DexLibraryClass> libraryClasses() {
-    assert lazyClassCollection == null : "Only allowed in non-lazy scenarios.";
-    List<DexLibraryClass> result = new ArrayList<>();
+  public List<DexProgramClass> classes() {
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
+    assert reorderClasses(classes);
+    return classes;
+  }
+
+  public List<DexLibraryClass> libraryClasses() {
+    assert classpathClasses == null : "Operation is not supported.";
+    Map<DexType, DexClass> classMap = forceLoadAllClasses();
+    List<DexLibraryClass> classes = new ArrayList<>();
     for (DexClass clazz : classMap.values()) {
       if (clazz.isLibraryClass()) {
-        result.add(clazz.asLibraryClass());
+        classes.add(clazz.asLibraryClass());
       }
     }
-    return result;
+    assert reorderClasses(classes);
+    return classes;
+  }
+
+  private Map<DexType, DexClass> forceLoadAllClasses() {
+    Map<DexType, DexClass> loaded = new IdentityHashMap<>();
+
+    // program classes are supposed to be loaded, but force-loading them is no-op.
+    programClasses.forceLoad(type -> true);
+    programClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+
+    if (classpathClasses != null) {
+      classpathClasses.forceLoad(type -> !loaded.containsKey(type));
+      classpathClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    if (libraryClasses != null) {
+      libraryClasses.forceLoad(type -> !loaded.containsKey(type));
+      libraryClasses.collectLoadedClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    }
+
+    return loaded;
   }
 
   public DexClass definitionFor(DexType type) {
-    DexClass clazz = classMap.get(type);
-    // In presence of lazy class collection we also reach out to it
-    // as well unless the class found is already a program class.
-    if (lazyClassCollection != null && (clazz == null || !clazz.isProgramClass())) {
-      clazz = lazyClassCollection.get(type, clazz);
+    DexClass clazz = programClasses.get(type);
+    if (clazz == null && classpathClasses != null) {
+      clazz = classpathClasses.get(type);
+    }
+    if (clazz == null && libraryClasses != null) {
+      clazz = libraryClasses.get(type);
     }
     return clazz;
   }
 
   public DexProgramClass programDefinitionFor(DexType type) {
-    DexClass clazz = classMap.get(type);
-    // Don't bother about lazy class collection, it should never load program classes.
-    return (clazz == null || !clazz.isProgramClass()) ? null : clazz.asProgramClass();
+    DexClass clazz = programClasses.get(type);
+    return clazz == null ? null : clazz.asProgramClass();
   }
 
   public String toString() {
-    return "Application (classes #" + classMap.size() + ")";
+    return "Application (" + programClasses + "; " + classpathClasses + "; " + libraryClasses + ")";
   }
 
   public ClassNameMapper getProguardMap() {
@@ -170,7 +192,7 @@
    * <p>If no directory is provided everything is written to System.out.
    */
   public void disassemble(Path outputDir, InternalOptions options) {
-    for (DexClass clazz : classes()) {
+    for (DexProgramClass clazz : programClasses.collectLoadedClasses()) {
       for (DexEncodedMethod method : clazz.virtualMethods()) {
         if (options.methodMatchesFilter(method)) {
           disassemble(method, getProguardMap(), outputDir);
@@ -224,7 +246,7 @@
    * Write smali source for the application code on the provided PrintStream.
    */
   public void smali(InternalOptions options, PrintStream ps) {
-    List<DexProgramClass> classes = (List<DexProgramClass>) classes();
+    List<DexProgramClass> classes = programClasses.collectLoadedClasses();
     classes.sort(Comparator.comparing(DexProgramClass::toSourceString));
     boolean firstClass = true;
     for (DexClass clazz : classes) {
@@ -273,9 +295,16 @@
   }
 
   public static class Builder {
+    // We handle program class collection separately from classpath
+    // and library class collections. Since while we assume program
+    // class collection should always be fully loaded and thus fully
+    // represented by the map (making it easy, for example, adding
+    // new or removing existing classes), classpath and library
+    // collections will be considered monolithic collections.
 
-    private final Hashtable<DexType, DexClass> classMap = new Hashtable<>();
-    private LazyClassCollection lazyClassCollection;
+    private final List<DexProgramClass> programClasses;
+    private ClasspathClassCollection classpathClasses;
+    private LibraryClassCollection libraryClasses;
 
     public final Hashtable<DexCode, DexCode> codeItems = new Hashtable<>();
 
@@ -287,18 +316,17 @@
     private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
 
     public Builder(DexItemFactory dexItemFactory, Timing timing) {
+      this.programClasses = new ArrayList<>();
       this.dexItemFactory = dexItemFactory;
       this.timing = timing;
-      this.lazyClassCollection = null;
+      this.classpathClasses = null;
+      this.libraryClasses = null;
     }
 
     public Builder(DexApplication application) {
-      this(application, application.classMap);
-    }
-
-    public Builder(DexApplication application, Map<DexType, DexClass> classMap) {
-      this.classMap.putAll(classMap);
-      this.lazyClassCollection = application.lazyClassCollection;
+      programClasses = application.programClasses.collectLoadedClasses();
+      classpathClasses = application.classpathClasses;
+      libraryClasses = application.libraryClasses;
       proguardMap = application.proguardMap;
       timing = application.timing;
       highestSortingString = application.highestSortingString;
@@ -312,58 +340,45 @@
       return this;
     }
 
+    public synchronized Builder replaceProgramClasses(List<DexProgramClass> newProgramClasses) {
+      assert newProgramClasses != null;
+      this.programClasses.clear();
+      this.programClasses.addAll(newProgramClasses);
+      return this;
+    }
+
     public synchronized Builder setHighestSortingString(DexString value) {
       highestSortingString = value;
       return this;
     }
 
-    public Builder addClass(DexClass clazz) {
-      addClass(clazz, false);
+    public synchronized Builder addProgramClass(DexProgramClass clazz) {
+      programClasses.add(clazz);
       return this;
     }
 
-    public Builder setLazyClassCollection(LazyClassCollection lazyClassMap) {
-      this.lazyClassCollection = lazyClassMap;
+    public Builder setClasspathClassCollection(ClasspathClassCollection classes) {
+      this.classpathClasses = classes;
       return this;
     }
 
-    public Builder addClassIgnoringLibraryDuplicates(DexClass clazz) {
-      addClass(clazz, true);
+    public Builder setLibraryClassCollection(LibraryClassCollection classes) {
+      this.libraryClasses = classes;
       return this;
     }
 
-    public Builder addSynthesizedClass(DexProgramClass synthesizedClass, boolean addToMainDexList) {
+    public synchronized Builder addSynthesizedClass(
+        DexProgramClass synthesizedClass, boolean addToMainDexList) {
       assert synthesizedClass.isProgramClass() : "All synthesized classes must be program classes";
-      addClass(synthesizedClass);
+      addProgramClass(synthesizedClass);
       if (addToMainDexList && !mainDexList.isEmpty()) {
         mainDexList.add(synthesizedClass.type);
       }
       return this;
     }
 
-    public List<DexProgramClass> getProgramClasses() {
-      List<DexProgramClass> result = new ArrayList<>();
-      // Note: we ignore lazy class collection because it
-      // is never supposed to be used for program classes.
-      for (DexClass clazz : classMap.values()) {
-        if (clazz.isProgramClass()) {
-          result.add(clazz.asProgramClass());
-        }
-      }
-      return result;
-    }
-
-    // Callback from FileReader when parsing a DexProgramClass (multi-threaded).
-    public synchronized void addClass(DexClass newClass, boolean skipLibDups) {
-      assert newClass != null;
-      DexType type = newClass.type;
-      DexClass oldClass = classMap.get(type);
-      if (oldClass != null) {
-        newClass = chooseClass(newClass, oldClass, skipLibDups);
-      }
-      if (oldClass != newClass) {
-        classMap.put(type, newClass);
-      }
+    public Collection<DexProgramClass> getProgramClasses() {
+      return programClasses;
     }
 
     public Builder addToMainDexList(Collection<DexType> mainDexList) {
@@ -374,83 +389,13 @@
     public DexApplication build() {
       return new DexApplication(
           proguardMap,
-          ImmutableMap.copyOf(classMap),
-          lazyClassCollection,
+          ProgramClassCollection.create(programClasses),
+          classpathClasses,
+          libraryClasses,
           ImmutableSet.copyOf(mainDexList),
           dexItemFactory,
           highestSortingString,
           timing);
     }
   }
-
-  public static DexClass chooseClass(DexClass a, DexClass b, boolean skipLibDups) {
-    // NOTE: We assume that there should not be any conflicting names in user defined
-    // classes and/or linked jars. If we ever want to allow 'keep first'-like policy
-    // to resolve this kind of conflict between program and/or classpath classes, we'll
-    // need to make sure we choose the class we keep deterministically.
-    if (a.isProgramClass() && b.isProgramClass()) {
-      if (allowProgramClassConflict(a.asProgramClass(), b.asProgramClass())) {
-        return a;
-      }
-      throw new CompilationError("Program type already present: " + a.type.toSourceString());
-    }
-    if (a.isProgramClass()) {
-      return chooseClass(a.asProgramClass(), b);
-    }
-    if (b.isProgramClass()) {
-      return chooseClass(b.asProgramClass(), a);
-    }
-
-    if (a.isClasspathClass() && b.isClasspathClass()) {
-      throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
-    }
-    if (a.isClasspathClass()) {
-      return chooseClass(a.asClasspathClass(), b.asLibraryClass());
-    }
-    if (b.isClasspathClass()) {
-      return chooseClass(b.asClasspathClass(), a.asLibraryClass());
-    }
-
-    return chooseClasses(b.asLibraryClass(), a.asLibraryClass(), skipLibDups);
-  }
-
-  private static boolean allowProgramClassConflict(DexProgramClass a, DexProgramClass b) {
-    // Currently only allow collapsing synthetic lambda classes.
-    return a.getOrigin() == Resource.Kind.DEX
-        && b.getOrigin() == Resource.Kind.DEX
-        && a.accessFlags.isSynthetic()
-        && b.accessFlags.isSynthetic()
-        && LambdaRewriter.hasLambdaClassPrefix(a.type)
-        && LambdaRewriter.hasLambdaClassPrefix(b.type);
-  }
-
-  private static DexClass chooseClass(DexProgramClass selected, DexClass ignored) {
-    assert !ignored.isProgramClass();
-    if (ignored.isLibraryClass()) {
-      logIgnoredClass(ignored, "Class `%s` was specified as library and program type.");
-    }
-    // We don't log program/classpath class conflict since it is expected case.
-    return selected;
-  }
-
-  private static DexClass chooseClass(DexClasspathClass selected, DexLibraryClass ignored) {
-    logIgnoredClass(ignored, "Class `%s` was specified as library and classpath type.");
-    return selected;
-  }
-
-  private static DexClass chooseClasses(
-      DexLibraryClass selected, DexLibraryClass ignored, boolean skipDups) {
-    if (!skipDups) {
-      throw new CompilationError(
-          "Library type already present: " + selected.type.toSourceString());
-    }
-    logIgnoredClass(ignored, "Class `%s` was specified twice as a library type.");
-    return selected;
-  }
-
-  private static void logIgnoredClass(DexClass ignored, String message) {
-    if (Log.ENABLED) {
-      Log.warn(DexApplication.class, message, ignored.type.toSourceString());
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 188440d..965d770 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -22,7 +22,7 @@
 import com.android.tools.r8.ir.code.MoveType;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
@@ -102,7 +102,7 @@
     return compilationState == PROCESSED_INLINING_CANDIDATE_PUBLIC;
   }
 
-  public void markProcessed(InliningConstraint state) {
+  public void markProcessed(Constraint state) {
     switch (state) {
       case ALWAYS:
         compilationState = PROCESSED_INLINING_CANDIDATE_PUBLIC;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Argument.java b/src/main/java/com/android/tools/r8/ir/code/Argument.java
index 94c415e..94f6507 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Argument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Argument.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
 
 /**
@@ -67,7 +67,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 2bef174..ab2056d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -119,7 +120,7 @@
   public List<BasicBlock> getNormalPredecessors() {
     ImmutableList.Builder<BasicBlock> normals = ImmutableList.builder();
     for (BasicBlock predecessor : predecessors) {
-      if (!predecessor.isCatchSuccessor(this)) {
+      if (!predecessor.hasCatchSuccessor(this)) {
         normals.add(predecessor);
       }
     }
@@ -647,10 +648,10 @@
 
   public EdgeType getEdgeType(BasicBlock successor) {
     assert successors.indexOf(successor) >= 0;
-    return isCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
+    return hasCatchSuccessor(successor) ? EdgeType.EXCEPTIONAL : EdgeType.NORMAL;
   }
 
-  public boolean isCatchSuccessor(BasicBlock block) {
+  public boolean hasCatchSuccessor(BasicBlock block) {
     if (!hasCatchHandlers()) {
       return false;
     }
@@ -658,7 +659,7 @@
   }
 
   public int guardsForCatchSuccessor(BasicBlock block) {
-    assert isCatchSuccessor(block);
+    assert hasCatchSuccessor(block);
     int index = successors.indexOf(block);
     int count = 0;
     for (int handler : catchHandlers.getAllTargets()) {
@@ -713,7 +714,7 @@
   }
 
   private String predecessorPostfix(BasicBlock block) {
-    if (isCatchSuccessor(block)) {
+    if (hasCatchSuccessor(block)) {
       return new String(new char[guardsForCatchSuccessor(block)]).replace("\0", "*");
     }
     return "";
@@ -1053,7 +1054,7 @@
     // one new phi to merge the two exception values, and all other phis don't need
     // to be changed.
     for (BasicBlock catchSuccessor : catchSuccessors) {
-      catchSuccessor.splitCriticalExceptioEdges(
+      catchSuccessor.splitCriticalExceptionEdges(
           code.valueNumberGenerator,
           newBlock -> {
             newBlock.setNumber(code.blocks.size());
@@ -1062,15 +1063,6 @@
     }
   }
 
-  private boolean allPredecessorsHaveCatchEdges() {
-    for (BasicBlock predecessor : getPredecessors()) {
-      if (!predecessor.isCatchSuccessor(this)) {
-        assert false;
-      }
-    }
-    return true;
-  }
-
   /**
    * Assumes that `this` block is a catch handler target (note that it does not have to
    * start with MoveException instruction, since the instruction can be removed by
@@ -1086,10 +1078,8 @@
    *
    * NOTE: onNewBlock must assign block number to the newly created block.
    */
-  public void splitCriticalExceptioEdges(
+  public void splitCriticalExceptionEdges(
       ValueNumberGenerator valueNumberGenerator, Consumer<BasicBlock> onNewBlock) {
-    assert allPredecessorsHaveCatchEdges();
-
     List<BasicBlock> predecessors = this.getPredecessors();
     boolean hasMoveException = entry().isMoveException();
     MoveException move = null;
@@ -1103,6 +1093,10 @@
     List<BasicBlock> newPredecessors = new ArrayList<>();
     List<Value> values = new ArrayList<>(predecessors.size());
     for (BasicBlock predecessor : predecessors) {
+      if (!predecessor.hasCatchSuccessor(this)) {
+        throw new CompilationError(
+            "Invalid block structure: catch block reachable via non-exceptional flow.");
+      }
       BasicBlock newBlock = new BasicBlock();
       newPredecessors.add(newBlock);
       if (hasMoveException) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Binop.java b/src/main/java/com/android/tools/r8/ir/code/Binop.java
index 83240bb..899516a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Binop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Binop.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public abstract class Binop extends Instruction {
 
@@ -112,7 +112,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 451832f..4cca9dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -15,7 +15,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.NumberUtils;
 
 public class ConstNumber extends ConstInstruction {
@@ -188,7 +188,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Div.java b/src/main/java/com/android/tools/r8/ir/code/Div.java
index c8c1c32..8fd9bf0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Div.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Div.java
@@ -14,9 +14,6 @@
 import com.android.tools.r8.code.DivIntLit8;
 import com.android.tools.r8.code.DivLong;
 import com.android.tools.r8.code.DivLong2Addr;
-import com.android.tools.r8.graph.AppInfo;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
 
 public class Div extends ArithmeticBinop {
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index ba1e0e9..0b8d876 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -9,8 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import java.util.List;
 
 abstract class FieldInstruction extends Instruction {
@@ -43,7 +42,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
     // Resolve the field if possible and decide whether the instruction can inlined.
     DexType fieldHolder = field.getHolder();
     DexEncodedField target = info.lookupInstanceTarget(fieldHolder, field);
@@ -51,15 +50,15 @@
     if ((target != null) && (fieldClass != null) && !fieldClass.isLibraryClass()) {
       DexAccessFlags flags = target.accessFlags;
       if (flags.isPublic()) {
-        return Inliner.InliningConstraint.ALWAYS;
+        return Constraint.ALWAYS;
       }
       if (flags.isPrivate() && (fieldHolder == holder)) {
-        return Inliner.InliningConstraint.PRIVATE;
+        return Constraint.PRIVATE;
       }
       if (flags.isProtected() && (fieldHolder.isSamePackage(holder))) {
-        return Inliner.InliningConstraint.PACKAGE;
+        return Constraint.PACKAGE;
       }
     }
-    return Inliner.InliningConstraint.NEVER;
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
index 3fe498d..1126299 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceOf.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class InstanceOf extends Instruction {
 
@@ -75,14 +75,14 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
     DexClass targetClass = info.definitionFor(type());
     if (targetClass == null) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
     if (targetClass.accessFlags.isPublic()) {
-      return InliningConstraint.ALWAYS;
+      return Constraint.ALWAYS;
     }
-    return InliningConstraint.NEVER;
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 1b18ccc..2e821cf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.Value.DebugInfo;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.InternalOptions;
@@ -797,7 +797,7 @@
   }
 
   // Returns the inlining constraint for this instruction.
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.NEVER;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.NEVER;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 8d0fa34..87edec8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -91,8 +91,14 @@
   }
 
   protected int argumentRegisterValue(int i, DexBuilder builder) {
+    assert requiredArgumentRegisters() > 5;
     if (i < arguments().size()) {
-      return builder.allocatedRegisterForRangedArgument(arguments().get(i), getNumber());
+      // If argument values flow into ranged invokes, all the ranged invoke arguments
+      // are arguments to this method in order. Therefore, we use the incoming registers
+      // for the ranged invoke arguments. We know that arguments are always available there.
+      // If argument reuse is allowed there is no splitting and if argument reuse is disallowed
+      // the argument registers are never overwritten.
+      return builder.argumentOrAllocateRegister(arguments().get(i), getNumber());
     }
     return 0;
   }
@@ -120,10 +126,10 @@
 
   protected boolean argumentsConsecutive(DexBuilder builder) {
     Value value = arguments().get(0);
-    int next = builder.allocatedRegisterForRangedArgument(value, getNumber()) + value.requiredRegisters();
+    int next = builder.argumentOrAllocateRegister(value, getNumber()) + value.requiredRegisters();
     for (int i = 1; i < arguments().size(); i++) {
       value = arguments().get(i);
-      assert next == builder.allocatedRegisterForRangedArgument(value, getNumber());
+      assert next == builder.argumentOrAllocateRegister(value, getNumber());
       next += value.requiredRegisters();
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
index 2a02218..7b1c007 100644
--- a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
@@ -5,7 +5,7 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.List;
 
@@ -47,7 +47,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 869d1d0..b3e155b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -29,7 +29,15 @@
 
   @Override
   public void buildDex(DexBuilder builder) {
-    int object = builder.allocatedRegister(object(), getNumber());
+    // If the monitor object is an argument, we use the argument register for all the monitor
+    // enters and exits in order to not confuse the Art verifier lock verification code.
+    // This is best effort. If the argument happens to be in a very high register we cannot
+    // do it and the lock verification can hit a case where it gets confused. Not much we
+    // can do about that, but this should avoid it in the most common cases.
+    int object = builder.argumentOrAllocateRegister(object(), getNumber());
+    if (object > maxInValueRegister()) {
+      object = builder.allocatedRegister(object(), getNumber());
+    }
     if (type == Type.ENTER) {
       builder.add(this, new MonitorEnter(object));
     } else {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 6f70c57..91e19f0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Move extends Instruction {
 
@@ -81,7 +81,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 5912f0f..de44e83 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.BasicBlock.EdgeType;
 import com.android.tools.r8.ir.conversion.IRBuilder;
@@ -60,6 +61,9 @@
     // exactly once by adding the operands.
     assert operands.isEmpty();
     boolean canBeNull = false;
+    if (block.getPredecessors().size() == 0) {
+      throwUndefinedValueError();
+    }
     for (BasicBlock pred : block.getPredecessors()) {
       EdgeType edgeType = pred.getEdgeType(block);
       // Since this read has been delayed we must provide the local info for the value.
@@ -79,6 +83,9 @@
     // exactly once by adding the operands.
     assert this.operands.isEmpty();
     boolean canBeNull = false;
+    if (operands.size() == 0) {
+      throwUndefinedValueError();
+    }
     for (Value operand : operands) {
       canBeNull |= operand.canBeNull();
       appendOperand(operand);
@@ -89,6 +96,13 @@
     removeTrivialPhi();
   }
 
+  private void throwUndefinedValueError() {
+    throw new CompilationError(
+        "Undefined value encountered during compilation. "
+            + "This is typically caused by invalid dex input that uses a register "
+            + "that is not define on all control-flow paths leading to the use.");
+  }
+
   private void appendOperand(Value operand) {
     operands.add(operand);
     operand.addPhiUser(this);
@@ -174,9 +188,6 @@
       same = op;
     }
     assert isTrivialPhi();
-    if (same == null) {
-      same = Value.UNDEFINED;
-    }
     // Removing this phi, so get rid of it as a phi user from all of the operands to avoid
     // recursively getting back here with the same phi. If the phi has itself as an operand
     // that also removes the self-reference.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 2819c58..142206c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Return extends JumpInstruction {
 
@@ -110,7 +110,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Throw.java b/src/main/java/com/android/tools/r8/ir/code/Throw.java
index e0ba866..9bd6a81 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Throw.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Throw.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 public class Throw extends JumpInstruction {
 
@@ -63,7 +63,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Unop.java b/src/main/java/com/android/tools/r8/ir/code/Unop.java
index ff1744a..174b7b6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Unop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Unop.java
@@ -6,7 +6,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 
 abstract public class Unop extends Instruction {
 
@@ -43,7 +43,7 @@
   }
 
   @Override
-  public InliningConstraint inliningConstraint(AppInfo info, DexType holder) {
-    return InliningConstraint.ALWAYS;
+  public Constraint inliningConstraint(AppInfo info, DexType holder) {
+    return Constraint.ALWAYS;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 9cf14d8..50afe5f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -318,8 +318,10 @@
     return registerAllocator.getRegisterForValue(value, instructionNumber);
   }
 
-  public int allocatedRegisterForRangedArgument(Value value, int instructionNumber) {
-    return registerAllocator.getRegisterForRangedArgument(value, instructionNumber);
+  // Get the argument register for a value if it is an argument, otherwise returns the
+  // allocated register at the instruction number.
+  public int argumentOrAllocateRegister(Value value, int instructionNumber) {
+    return registerAllocator.getArgumentOrAllocateRegisterForValue(value, instructionNumber);
   }
 
   public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
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 b3d5266..d24c8b4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -81,8 +81,6 @@
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.StringUtils.BraceType;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
@@ -372,9 +370,6 @@
     // necessary.
     splitCriticalEdges();
 
-    // Consistency check.
-    assert phiOperandsAreConsistent();
-
     // Package up the IR code.
     IRCode ir = new IRCode(method, blocks, normalExitBlock, valueNumberGenerator);
 
@@ -808,7 +803,7 @@
   public void addGoto(int targetOffset) {
     addInstruction(new Goto());
     BasicBlock targetBlock = getTarget(targetOffset);
-    if (currentBlock.isCatchSuccessor(targetBlock)) {
+    if (currentBlock.hasCatchSuccessor(targetBlock)) {
       needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, targetBlock));
     } else {
       currentBlock.link(targetBlock);
@@ -1086,7 +1081,12 @@
     Value out = writeRegister(dest, MoveType.OBJECT, ThrowingInfo.NO_THROW);
     MoveException instruction = new MoveException(out);
     assert !instruction.instructionTypeCanThrow();
-    assert currentBlock.getInstructions().isEmpty();
+    if (!currentBlock.getInstructions().isEmpty()) {
+      throw new CompilationError("Invalid MoveException instruction encountered. "
+          + "The MoveException instruction is not the first instruction in the block in "
+          + method.qualifiedName()
+          + ".");
+    }
     addInstruction(instruction);
   }
 
@@ -1698,7 +1698,7 @@
     assert currentBlock != null;
     flushCurrentDebugPosition();
     currentBlock.add(new Goto());
-    if (currentBlock.isCatchSuccessor(nextBlock)) {
+    if (currentBlock.hasCatchSuccessor(nextBlock)) {
       needGotoToCatchBlocks.add(new BasicBlock.Pair(currentBlock, nextBlock));
     } else {
       currentBlock.link(nextBlock);
@@ -1778,8 +1778,8 @@
       target.getPredecessors().add(newBlock);
 
       // Check that the successor indexes are correct.
-      assert source.isCatchSuccessor(newBlock);
-      assert !source.isCatchSuccessor(target);
+      assert source.hasCatchSuccessor(newBlock);
+      assert !source.hasCatchSuccessor(target);
 
       // Mark the filled predecessors to the blocks.
       if (source.isFilled()) {
@@ -1790,22 +1790,6 @@
     return blockNumber;
   }
 
-  private boolean phiOperandsAreConsistent() {
-    for (BasicBlock block : blocks) {
-      if (block.hasIncompletePhis()) {
-        StringBuilder builder = new StringBuilder("Incomplete phis in ");
-        builder.append(method);
-        builder.append(". The following registers appear to be uninitialized: ");
-        StringUtils.append(builder, block.getIncompletePhiRegisters(), ", ", BraceType.NONE);
-        throw new CompilationError(builder.toString());
-      }
-      for (Phi phi : block.getPhis()) {
-        assert phi.getOperands().size() == block.getPredecessors().size();
-      }
-    }
-    return true;
-  }
-
   /**
    * Change to control-flow graph to avoid repeated phi operands when all the same values
    * flow in from multiple predecessors.
@@ -1837,6 +1821,15 @@
   public void joinPredecessorsWithIdenticalPhis() {
     List<BasicBlock> blocksToAdd = new ArrayList<>();
     for (BasicBlock block : blocks) {
+      // Consistency check. At this point there should be no incomplete phis.
+      // If there are, the input is typically dex code that uses a register
+      // that is not defined on all control-flow paths.
+      if (block.hasIncompletePhis()) {
+        throw new CompilationError(
+            "Undefined value encountered during compilation. "
+                + "This is typically caused by invalid dex input that uses a register "
+                + "that is not define on all control-flow paths leading to the use.");
+      }
       if (block.entry() instanceof MoveException) {
         // TODO: Should we support joining in the presence of move-exception instructions?
         continue;
@@ -1889,7 +1882,7 @@
       // If any of the edges to the block are critical, we need to insert new blocks on each
       // containing the move-exception instruction which must remain the first instruction.
       if (block.entry() instanceof MoveException) {
-        block.splitCriticalExceptioEdges(valueNumberGenerator,
+        block.splitCriticalExceptionEdges(valueNumberGenerator,
             newBlock -> {
               newBlock.setNumber(blocks.size() + newBlocks.size());
               newBlocks.add(newBlock);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 8370eb8..3b19b02 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -25,7 +25,7 @@
 import com.android.tools.r8.ir.optimize.CodeRewriter;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.Inliner;
-import com.android.tools.r8.ir.optimize.Inliner.InliningConstraint;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.MemberValuePropagation;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
@@ -390,15 +390,15 @@
     boolean matchesMethodFilter = options.methodMatchesFilter(method);
     if (code != null && matchesMethodFilter) {
       assert !method.isProcessed();
-      InliningConstraint state = rewriteCode(method, outlineHandler);
+      Constraint state = rewriteCode(method, outlineHandler);
       method.markProcessed(state);
     } else {
       // Mark abstract methods as processed as well.
-      method.markProcessed(InliningConstraint.NEVER);
+      method.markProcessed(Constraint.NEVER);
     }
   }
 
-  private InliningConstraint rewriteCode(DexEncodedMethod method,
+  private Constraint rewriteCode(DexEncodedMethod method,
       BiConsumer<IRCode, DexEncodedMethod> outlineHandler) {
     if (options.verbose) {
       System.out.println("Processing: " + method.toSourceString());
@@ -409,7 +409,7 @@
     }
     IRCode code = method.buildIR(options);
     if (code == null) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
     if (Log.ENABLED) {
       Log.debug(getClass(), "Initial (SSA) flow graph for %s:\n%s", method.toSourceString(), code);
@@ -490,7 +490,7 @@
 
     // After all the optimizations have take place, we compute whether method should be inlined.
     if (!options.inlineAccessors || inliner == null) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
     return inliner.identifySimpleMethods(code, method);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index f677637..f57c022 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -50,23 +50,23 @@
     this.options = options;
   }
 
-  private InliningConstraint instructionAllowedForInlining(
+  private Constraint instructionAllowedForInlining(
       DexEncodedMethod method, Instruction instruction) {
-    InliningConstraint result = instruction.inliningConstraint(appInfo, method.method.holder);
-    if ((result == InliningConstraint.NEVER) && instruction.isDebugInstruction()) {
-      return InliningConstraint.ALWAYS;
+    Constraint result = instruction.inliningConstraint(appInfo, method.method.holder);
+    if ((result == Constraint.NEVER) && instruction.isDebugInstruction()) {
+      return Constraint.ALWAYS;
     }
     return result;
   }
 
-  public InliningConstraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
+  public Constraint identifySimpleMethods(IRCode code, DexEncodedMethod method) {
     DexCode dex = method.getCode().asDexCode();
     // We have generated code for a method and we want to figure out whether the method is a
     // candidate for inlining. The code is the final IR after optimizations.
     if (dex.instructions.length > INLINING_INSTRUCTION_LIMIT) {
-      return InliningConstraint.NEVER;
+      return Constraint.NEVER;
     }
-    InliningConstraint result = InliningConstraint.ALWAYS;
+    Constraint result = Constraint.ALWAYS;
     ListIterator<BasicBlock> iterator = code.listIterator();
     assert iterator.hasNext();
     BasicBlock block = iterator.next();
@@ -76,9 +76,9 @@
       InstructionListIterator it = block.listIterator();
       while (it.hasNext()) {
         Instruction instruction = it.next();
-        InliningConstraint state = instructionAllowedForInlining(method, instruction);
-        if (state == InliningConstraint.NEVER) {
-          return InliningConstraint.NEVER;
+        Constraint state = instructionAllowedForInlining(method, instruction);
+        if (state == Constraint.NEVER) {
+          return Constraint.NEVER;
         }
         if (state.ordinal() < result.ordinal()) {
           result = state;
@@ -105,7 +105,7 @@
     return methodHolder.isSamePackage(targetHolder);
   }
 
-  public enum InliningConstraint {
+  public enum Constraint {
     // The ordinal values are important so please do not reorder.
     NEVER,    // Never inline this.
     PRIVATE,  // Only inline this into methods with same holder.
@@ -113,23 +113,27 @@
     ALWAYS,   // No restrictions for inlining this.
   }
 
+  public enum Reason {
+    FORCE,         // Inlinee is marked for forced inlining (bridge method or renamed constructor).
+    SINGLE_CALLER, // Inlinee has precisely one caller.
+    DUAL_CALLER,   // Inlinee has precisely two callers.
+    SIMPLE,        // Inlinee has simple code suitable for inlining.
+  }
+
   static public class InlineAction {
 
     public final DexEncodedMethod target;
     public final Invoke invoke;
-    public final boolean forceInline;
+    public final Reason reason;
 
-    public InlineAction(
-        DexEncodedMethod target, Invoke invoke, boolean forceInline) {
+    public InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
       this.target = target;
       this.invoke = invoke;
-      this.forceInline = forceInline;
+      this.reason = reason;
     }
 
-    public InlineAction(DexEncodedMethod target, Invoke invoke) {
-      this.target = target;
-      this.invoke = invoke;
-      this.forceInline = target.getOptimizationInfo().forceInline();
+    public boolean forceInline() {
+      return reason != Reason.SIMPLE;
     }
 
     public IRCode buildIR(ValueNumberGenerator generator, AppInfoWithSubtyping appInfo,
@@ -275,7 +279,7 @@
               // Back up before the invoke instruction.
               iterator.previous();
               instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.forceInline) {
+              if (instruction_allowance >= 0 || result.forceInline()) {
                 iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
               }
               // If we inlined the invoke from a bridge method, it is no longer a bridge method.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
index 954f11c..7d83150 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningOracle.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.CallGraph;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.logging.Log;
 
 /**
@@ -103,6 +104,19 @@
     return candidate;
   }
 
+  private Reason computeInliningReason(DexEncodedMethod target) {
+    if (target.getOptimizationInfo().forceInline()) {
+      return Reason.FORCE;
+    }
+    if (callGraph.hasSingleCallSite(target)) {
+      return Reason.SINGLE_CALLER;
+    }
+    if (isDoubleInliningTarget(target)) {
+      return Reason.DUAL_CALLER;
+    }
+    return Reason.SIMPLE;
+  }
+
   public InlineAction computeForInvokeWithReceiver(InvokeMethodWithReceiver invoke) {
     boolean receiverIsNeverNull = invoke.receiverIsNeverNull();
     if (!receiverIsNeverNull) {
@@ -120,11 +134,9 @@
       return null;
     }
 
-    boolean fromBridgeMethod = target.getOptimizationInfo().forceInline();
-
     if (target == method) {
       // Bridge methods should never have recursive calls.
-      assert !fromBridgeMethod;
+      assert !target.getOptimizationInfo().forceInline();
       return null;
     }
 
@@ -154,11 +166,9 @@
       return null;
     }
 
-    boolean doubleInlineTarget = isDoubleInliningTarget(target);
+    Reason reason = computeInliningReason(target);
     // Determine if this should be inlined no matter how big it is.
-    boolean forceInline =
-        fromBridgeMethod | callGraph.hasSingleCallSite(target) | doubleInlineTarget;
-    if (!target.isInliningCandidate(method, forceInline)) {
+    if (!target.isInliningCandidate(method, reason != Reason.SIMPLE)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -175,7 +185,7 @@
     }
 
     // Attempt to inline a candidate that is only called twice.
-    if (doubleInlineTarget && (doubleInlining(target) == null)) {
+    if ((reason == Reason.DUAL_CALLER) && (doubleInlining(target) == null)) {
       if (info != null) {
         info.exclude(invoke, "target is not ready for double inlining");
       }
@@ -185,7 +195,7 @@
     if (info != null) {
       info.include(invoke.getType(), target);
     }
-    return new InlineAction(target, invoke, forceInline);
+    return new InlineAction(target, invoke, reason);
   }
 
   public InlineAction computeForInvokeVirtual(InvokeVirtual invoke) {
@@ -224,12 +234,9 @@
     if (candidate == null) {
       return null;
     }
-    boolean fromBridgeMethod = candidate.getOptimizationInfo().forceInline();
-    boolean doubleInlineTarget = isDoubleInliningTarget(candidate);
+    Reason reason = computeInliningReason(candidate);
     // Determine if this should be inlined no matter how big it is.
-    boolean forceInline =
-        fromBridgeMethod | callGraph.hasSingleCallSite(candidate) | doubleInlineTarget;
-    if (!candidate.isInliningCandidate(method, forceInline)) {
+    if (!candidate.isInliningCandidate(method, reason != Reason.SIMPLE)) {
       // Abort inlining attempt if the single target is not an inlining candidate.
       if (info != null) {
         info.exclude(invoke, "target is not identified for inlining");
@@ -246,7 +253,7 @@
     }
 
     // Attempt to inline a candidate that is only called twice.
-    if (doubleInlineTarget && (doubleInlining(candidate) == null)) {
+    if ((reason == Reason.DUAL_CALLER) && (doubleInlining(candidate) == null)) {
       if (info != null) {
         info.exclude(invoke, "target is not ready for double inlining");
       }
@@ -256,7 +263,7 @@
     if (info != null) {
       info.include(invoke.getType(), candidate);
     }
-    return new InlineAction(candidate, invoke);
+    return new InlineAction(candidate, invoke, reason);
   }
 
   public InlineAction computeForInvokeSuper(InvokeSuper invoke) {
@@ -270,7 +277,7 @@
     if (info != null) {
       info.include(invoke.getType(), candidate);
     }
-    return new InlineAction(candidate, invoke);
+    return new InlineAction(candidate, invoke, Reason.SIMPLE);
   }
 
   public InlineAction computeForInvokePolymorpic(InvokePolymorphic invoke) {
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 4bf5dc6..a317701 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
@@ -421,12 +421,7 @@
   }
 
   @Override
-  public int getRegisterForRangedArgument(Value value, int instructionNumber) {
-    // If argument values flow into ranged invokes, all the ranged invoke arguments
-    // are arguments to this method in order. Therefore, we use the incoming registers
-    // for the ranged invoke arguments. We know that arguments are always available there.
-    // If argument reuse is allowed there is no splitting and if argument reuse is disallowed
-    // the argument registers are never overwritten.
+  public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
     if (value.isArgument()) {
       return getRegisterForIntervals(value.getLiveIntervals());
     }
@@ -1414,7 +1409,8 @@
     assert spilled.isSpilled();
     assert spilled.getValue().isConstant();
     assert !spilled.isLinked() || spilled.isArgumentInterval();
-    int maxGapSize = 50 * INSTRUCTION_NUMBER_DELTA;
+    // Do not split range if constant is reused by one of the eleven following instruction.
+    int maxGapSize = 11 * INSTRUCTION_NUMBER_DELTA;
     if (!spilled.getUses().isEmpty()) {
       // Split at first use after the spill position and add to unhandled to get a register
       // assigned for rematerialization.
@@ -1520,7 +1516,7 @@
         // If we are processing an exception edge, we need to use the throwing instruction
         // as the instruction we are coming from.
         int fromInstruction = block.exit().getNumber();
-        boolean isCatch = block.isCatchSuccessor(successor);
+        boolean isCatch = block.hasCatchSuccessor(successor);
         if (isCatch) {
           for (Instruction instruction : block.getInstructions()) {
             if (instruction.instructionTypeCanThrow()) {
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
index fc7c2d8..79ba8db 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
@@ -10,5 +10,5 @@
   int registersUsed();
   int getRegisterForValue(Value value, int instructionNumber);
   boolean argumentValueUsesHighRegister(Value value, int instructionNumber);
-  int getRegisterForRangedArgument(Value value, int instructionNumber);
+  int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber);
 }
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 21ab0d7..04f6786 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -7,17 +7,14 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.Map;
+import java.util.List;
 import java.util.Set;
 
 public class TreePruner {
@@ -51,22 +48,20 @@
   }
 
   private DexApplication.Builder removeUnused(DexApplication application) {
-    return new DexApplication.Builder(application, removeUnusedClassStructure(application));
+    return new DexApplication.Builder(application)
+        .replaceProgramClasses(getNewProgramClasses(application.classes()));
   }
 
-  private Map<DexType, DexClass> removeUnusedClassStructure(DexApplication application) {
-    Map<DexType, DexClass> classMap = new IdentityHashMap<>();
-    for (DexLibraryClass clazz : application.libraryClasses()) {
-      classMap.put(clazz.type, clazz);
-    }
-    for (DexProgramClass clazz : application.classes()) {
+  private List<DexProgramClass> getNewProgramClasses(List<DexProgramClass> classes) {
+    List<DexProgramClass> newClasses = new ArrayList<>();
+    for (DexProgramClass clazz : classes) {
       if (!appInfo.liveTypes.contains(clazz.type) && !options.debugKeepRules) {
         // The class is completely unused and we can remove it.
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing class: " + clazz);
         }
       } else {
-        classMap.put(clazz.type, clazz);
+        newClasses.add(clazz);
         if (!appInfo.instantiatedTypes.contains(clazz.type) && !options.debugKeepRules) {
           // The class is only needed as a type but never instantiated. Make it abstract to reflect
           // this.
@@ -88,7 +83,7 @@
         clazz.staticFields = reachableFields(clazz.staticFields());
       }
     }
-    return classMap;
+    return newClasses;
   }
 
   private <S extends PresortedComparable<S>, T extends KeyedDexItem<S>> int firstUnreachableIndex(
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index b4fdfe9..389db15 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -7,8 +7,8 @@
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassKind;
@@ -53,8 +53,8 @@
   private final ImmutableList<Resource> programResources;
   private final ImmutableList<Resource> classpathResources;
   private final ImmutableList<Resource> libraryResources;
-  private final ImmutableList<ResourceProvider> classpathResourceProviders;
-  private final ImmutableList<ResourceProvider> libraryResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
+  private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
   private final Resource proguardMap;
   private final Resource proguardSeeds;
   private final Resource packageDistribution;
@@ -65,8 +65,8 @@
       ImmutableList<Resource> programResources,
       ImmutableList<Resource> classpathResources,
       ImmutableList<Resource> libraryResources,
-      ImmutableList<ResourceProvider> classpathResourceProviders,
-      ImmutableList<ResourceProvider> libraryResourceProviders,
+      ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
+      ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
       Resource proguardMap,
       Resource proguardSeeds,
       Resource packageDistribution,
@@ -176,12 +176,12 @@
   }
 
   /** Get classpath resource providers. */
-  public List<ResourceProvider> getClasspathResourceProviders() {
+  public List<ClassFileResourceProvider> getClasspathResourceProviders() {
     return classpathResourceProviders;
   }
 
   /** Get library resource providers. */
-  public List<ResourceProvider> getLibraryResourceProviders() {
+  public List<ClassFileResourceProvider> getLibraryResourceProviders() {
     return libraryResourceProviders;
   }
 
@@ -372,8 +372,8 @@
     private final List<Resource> programResources = new ArrayList<>();
     private final List<Resource> classpathResources = new ArrayList<>();
     private final List<Resource> libraryResources = new ArrayList<>();
-    private final List<ResourceProvider> classpathResourceProviders = new ArrayList<>();
-    private final List<ResourceProvider> libraryResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
+    private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
     private Resource proguardMap;
     private Resource proguardSeeds;
     private Resource packageDistribution;
@@ -456,7 +456,7 @@
     /**
      * Add classpath resource provider.
      */
-    public Builder addClasspathResourceProvider(ResourceProvider provider) {
+    public Builder addClasspathResourceProvider(ClassFileResourceProvider provider) {
       classpathResourceProviders.add(provider);
       return this;
     }
@@ -481,7 +481,7 @@
     /**
      * Add library resource provider.
      */
-    public Builder addLibraryResourceProvider(ResourceProvider provider) {
+    public Builder addLibraryResourceProvider(ClassFileResourceProvider provider) {
       libraryResourceProviders.add(provider);
       return this;
     }
@@ -641,7 +641,7 @@
                 Resource.Kind.DEX, ByteStreams.toByteArray(stream)));
           } else if (isClassFile(name)) {
             containsClassData = true;
-            String descriptor = PreloadedResourceProvider.guessTypeDescriptor(name);
+            String descriptor = PreloadedClassFileProvider.guessTypeDescriptor(name);
             resources(classKind).add(Resource.fromBytes(Resource.Kind.CLASSFILE,
                 ByteStreams.toByteArray(stream), Collections.singleton(descriptor)));
           }
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
new file mode 100644
index 0000000..d0f2cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Represents a collection of classes. Collection can be fully loaded,
+ * lazy loaded or have preloaded classes along with lazy loaded content.
+ */
+public abstract class ClassMap<T extends DexClass> {
+  // For each type which has ever been queried stores one class loaded from
+  // resources provided by different resource providers.
+  //
+  // NOTE: all access must be synchronized on `classes`.
+  private final Map<DexType, Value<T>> classes;
+
+  // Class provider if available. In case it's `null`, all classes of
+  // the collection must be pre-populated in `classes`.
+  private final ClassProvider<T> classProvider;
+
+  ClassMap(Map<DexType, Value<T>> classes, ClassProvider<T> classProvider) {
+    this.classes = classes == null ? new IdentityHashMap<>() : classes;
+    this.classProvider = classProvider;
+    assert this.classProvider == null || this.classProvider.getClassKind() == getClassKind();
+  }
+
+  /** Resolves a class conflict by selecting a class, may generate compilation error. */
+  abstract T resolveClassConflict(T a, T b);
+
+  /** Kind of the classes supported by this collection. */
+  abstract ClassKind getClassKind();
+
+  @Override
+  public String toString() {
+    synchronized (classes) {
+      return classes.size() + " loaded, provider: " +
+          (classProvider == null ? "none" : classProvider.toString());
+    }
+  }
+
+  /** Returns a definition for a class or `null` if there is no such class in the collection. */
+  public T get(DexType type) {
+    final Value<T> value = getOrCreateValue(type);
+
+    if (!value.ready) {
+      // Load the value in a context synchronized on value instance. This way
+      // we avoid locking the whole collection during expensive resource loading
+      // and classes construction operations.
+      synchronized (value) {
+        if (!value.ready && classProvider != null) {
+          classProvider.collectClass(type, clazz -> {
+            assert clazz != null;
+            assert getClassKind().isOfKind(clazz);
+            assert !value.ready;
+
+            if (clazz.type != type) {
+              throw new CompilationError("Class content provided for type descriptor "
+                  + type.toSourceString() + " actually defines class " + clazz.type
+                  .toSourceString());
+            }
+
+            if (value.clazz == null) {
+              value.clazz = clazz;
+            } else {
+              // The class resolution *may* generate a compilation error as one of
+              // possible resolutions. In this case we leave `value` in (false, null)
+              // state so in rare case of another thread trying to get the same class
+              // before this error is propagated it will get the same conflict.
+              T oldClass = value.clazz;
+              value.clazz = null;
+              value.clazz = resolveClassConflict(oldClass, clazz);
+            }
+          });
+        }
+        value.ready = true;
+      }
+    }
+
+    assert value.ready;
+    return value.clazz;
+  }
+
+  private Value<T> getOrCreateValue(DexType type) {
+    synchronized (classes) {
+      return classes.computeIfAbsent(type, k -> new Value<>());
+    }
+  }
+
+  /** Returns currently loaded classes */
+  public List<T> collectLoadedClasses() {
+    List<T> loadedClasses = new ArrayList<>();
+    synchronized (classes) {
+      for (Value<T> value : classes.values()) {
+        // Method collectLoadedClasses() must always be called when there
+        // is no risk of concurrent loading of the classes, otherwise the
+        // behaviour of this method is undefined. Note that value mutations
+        // are NOT synchronized in `classes`, so the assertion below does
+        // not enforce this requirement, but may help detect wrong behaviour.
+        assert value.ready : "";
+        if (value.clazz != null) {
+          loadedClasses.add(value.clazz);
+        }
+      }
+    }
+    return loadedClasses;
+  }
+
+  /** Forces loading of all the classes satisfying the criteria specified. */
+  public void forceLoad(Predicate<DexType> load) {
+    if (classProvider != null) {
+      Set<DexType> loaded;
+      synchronized (classes) {
+        loaded = classes.keySet();
+      }
+      Collection<DexType> types = classProvider.collectTypes();
+      for (DexType type : types) {
+        if (load.test(type) && !loaded.contains(type)) {
+          get(type); // force-load type.
+        }
+      }
+    }
+  }
+
+  // Represents a value in the class map.
+  final static class Value<T> {
+    volatile boolean ready;
+    T clazz;
+
+    Value() {
+      ready = false;
+      clazz = null;
+    }
+
+    Value(T clazz) {
+      this.clazz = clazz;
+      this.ready = true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClassProvider.java b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
new file mode 100644
index 0000000..25abdce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClassProvider.java
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.JarApplicationReader;
+import com.android.tools.r8.graph.JarClassFileReader;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.io.Closer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/** Represents a provider for classes loaded from different sources. */
+public abstract class ClassProvider<T extends DexClass> {
+  private final ClassKind classKind;
+
+  ClassProvider(ClassKind classKind) {
+    this.classKind = classKind;
+  }
+
+  /** The kind of the classes created by the provider. */
+  final ClassKind getClassKind() {
+    return classKind;
+  }
+
+  /**
+   * The provider uses the callback to return all the classes that might
+   * be associated with the descriptor asked for.
+   *
+   * NOTE: the provider is not required to cache created classes and this
+   * method may create a new class instance in case it is called twice for
+   * the same type. For this reason it is recommended that the provider
+   * user only calls this method once per any given type.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract void collectClass(DexType type, Consumer<T> classConsumer);
+
+  /**
+   * Returns all the types of classes that might be produced by this provider.
+   *
+   * NOTE: thread-safe.
+   */
+  public abstract Collection<DexType> collectTypes();
+
+  /** Create class provider for java class resource provider. */
+  public static <T extends DexClass> ClassProvider<T> forClassFileResources(
+      ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+    return new ClassFileResourceReader<>(classKind, provider, reader);
+  }
+
+  /** Create class provider for preloaded classes, classes may have conflicting names. */
+  public static <T extends DexClass> ClassProvider<T> forPreloadedClasses(
+      ClassKind classKind, Collection<T> classes) {
+    ImmutableListMultimap.Builder<DexType, T> builder = ImmutableListMultimap.builder();
+    for (T clazz : classes) {
+      builder.put(clazz.type, clazz);
+    }
+    return new PreloadedClassProvider<>(classKind, builder.build());
+  }
+
+  /** Create class provider for preloaded classes. */
+  public static <T extends DexClass> ClassProvider<T> combine(
+      ClassKind classKind, List<ClassProvider<T>> providers) {
+    return new CombinedClassProvider<>(classKind, providers);
+  }
+
+  private static class ClassFileResourceReader<T extends DexClass> extends ClassProvider<T> {
+    private final ClassKind classKind;
+    private final ClassFileResourceProvider provider;
+    private final JarApplicationReader reader;
+
+    private ClassFileResourceReader(
+        ClassKind classKind, ClassFileResourceProvider provider, JarApplicationReader reader) {
+      super(classKind);
+      this.classKind = classKind;
+      this.provider = provider;
+      this.reader = reader;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      String descriptor = type.descriptor.toString();
+      Resource resource = provider.getResource(descriptor);
+      if (resource != null) {
+        try (Closer closer = Closer.create()) {
+          JarClassFileReader classReader =
+              new JarClassFileReader(reader, classKind.bridgeConsumer(classConsumer));
+          classReader.read(DEFAULT_DEX_FILENAME, classKind, resource.getStream(closer));
+        } catch (IOException e) {
+          throw new CompilationError("Failed to load class: " + descriptor, e);
+        }
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      List<DexType> types = new ArrayList<>();
+      for (String descriptor : provider.getClassDescriptors()) {
+        types.add(reader.options.itemFactory.createType(descriptor));
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      return "class-resource-provider(" + provider.toString() + ")";
+    }
+  }
+
+  private static class PreloadedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final Multimap<DexType, T> classes;
+
+    private PreloadedClassProvider(ClassKind classKind, Multimap<DexType, T> classes) {
+      super(classKind);
+      this.classes = classes;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (T clazz : classes.get(type)) {
+        classConsumer.accept(clazz);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      return classes.keys();
+    }
+
+    @Override
+    public String toString() {
+      return "preloaded(" + classes.size() + ")";
+    }
+  }
+
+  private static class CombinedClassProvider<T extends DexClass> extends ClassProvider<T> {
+    private final List<ClassProvider<T>> providers;
+
+    private CombinedClassProvider(ClassKind classKind, List<ClassProvider<T>> providers) {
+      super(classKind);
+      this.providers = providers;
+    }
+
+    @Override
+    public void collectClass(DexType type, Consumer<T> classConsumer) {
+      for (ClassProvider<T> provider : providers) {
+        provider.collectClass(type, classConsumer);
+      }
+    }
+
+    @Override
+    public Collection<DexType> collectTypes() {
+      Set<DexType> types = Sets.newIdentityHashSet();
+      for (ClassProvider<T> provider : providers) {
+        types.addAll(provider.collectTypes());
+      }
+      return types;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder builder = new StringBuilder();
+      String prefix = "combined(";
+      for (ClassProvider<T> provider : providers) {
+        builder.append(prefix);
+        prefix = ", ";
+        builder.append(provider);
+      }
+      return builder.append(")").toString();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
new file mode 100644
index 0000000..ee00c39
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ClasspathClassCollection.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClasspathClass;
+
+/** Represents a collection of classpath classes. */
+public class ClasspathClassCollection extends ClassMap<DexClasspathClass> {
+  public ClasspathClassCollection(ClassProvider<DexClasspathClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexClasspathClass resolveClassConflict(DexClasspathClass a, DexClasspathClass b) {
+    throw new CompilationError("Classpath type already present: " + a.type.toSourceString());
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.CLASSPATH;
+  }
+
+  @Override
+  public String toString() {
+    return "classpath classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
new file mode 100644
index 0000000..c060ead
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/DirectoryClassFileProvider.java
@@ -0,0 +1,76 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+
+import com.android.tools.r8.ClassFileResourceProvider;
+import com.android.tools.r8.Resource;
+import com.google.common.collect.Sets;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Lazy resource provider returning class file resources based
+ * on filesystem directory content.
+ */
+public final class DirectoryClassFileProvider implements ClassFileResourceProvider {
+  private final Path root;
+
+  private DirectoryClassFileProvider(Path root) {
+    this.root = root;
+  }
+
+  @Override
+  public Set<String> getClassDescriptors() {
+    HashSet<String> result = Sets.newHashSet();
+    collectClassDescriptors(root, result);
+    return result;
+  }
+
+  private void collectClassDescriptors(Path dir, Set<String> result) {
+    File file = dir.toFile();
+    if (file.exists()) {
+      File[] files = file.listFiles();
+      if (files != null) {
+        for (File child : files) {
+          if (child.isDirectory()) {
+            collectClassDescriptors(child.toPath(), result);
+          } else {
+            String relative = root.relativize(child.toPath()).toString();
+            if (relative.endsWith(CLASS_EXTENSION)) {
+              result.add("L" + relative.substring(
+                  0, relative.length() - CLASS_EXTENSION.length()) + ";");
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public Resource getResource(String descriptor) {
+    assert DescriptorUtils.isClassDescriptor(descriptor);
+
+    // Build expected file path based on type descriptor.
+    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
+    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
+    File file = filePath.toFile();
+
+    return (file.exists() && !file.isDirectory())
+        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
+  }
+
+  /** Create resource provider from directory path. */
+  public static ClassFileResourceProvider fromDirectory(Path dir) {
+    return new DirectoryClassFileProvider(dir.toAbsolutePath());
+  }
+
+  @Override
+  public String toString() {
+    return "directory(" + root + ")";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java b/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
deleted file mode 100644
index b003d86..0000000
--- a/src/main/java/com/android/tools/r8/utils/DirectoryResourceProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.utils;
-
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * Lazy resource provider based on filesystem directory content.
- *
- * NOTE: only handles classfile resources.
- */
-public final class DirectoryResourceProvider implements ResourceProvider {
-  private final Path root;
-
-  private DirectoryResourceProvider(Path root) {
-    this.root = root;
-  }
-
-  @Override
-  public Resource getResource(String descriptor) {
-    assert DescriptorUtils.isClassDescriptor(descriptor);
-
-    // Build expected file path based on type descriptor.
-    String classBinaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(descriptor);
-    Path filePath = root.resolve(classBinaryName + CLASS_EXTENSION);
-    File file = filePath.toFile();
-
-    return (file.exists() && !file.isDirectory())
-        ? Resource.fromFile(Resource.Kind.CLASSFILE, filePath) : null;
-  }
-
-  /** Create resource provider from directory path. */
-  public static ResourceProvider fromDirectory(Path dir) {
-    return new DirectoryResourceProvider(dir.toAbsolutePath());
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java b/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
deleted file mode 100644
index d7379ef..0000000
--- a/src/main/java/com/android/tools/r8/utils/LazyClassCollection.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.utils;
-
-import static com.android.tools.r8.utils.FileUtils.DEFAULT_DEX_FILENAME;
-
-import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.ClassKind;
-import com.android.tools.r8.graph.DexApplication;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.JarApplicationReader;
-import com.android.tools.r8.graph.JarClassFileReader;
-import com.google.common.collect.ImmutableList;
-import com.google.common.io.Closer;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents a collection of classes loaded lazily from a set of lazy resource
- * providers. The collection is autonomous, it lazily collects classes but
- * does not choose the classes in cases of conflicts, delaying it until
- * the class is asked for.
- *
- * NOTE: only java class resources are allowed to be lazy loaded.
- */
-public final class LazyClassCollection {
-  // For each type which has ever been queried stores one or several classes loaded
-  // from resources provided by different resource providers. In majority of the
-  // cases there will only be one class per type. We store classes for all the
-  // resource providers and resolve the classes at the time it is queried.
-  //
-  // Must be synchronized on `classes`.
-  private final Map<DexType, DexClass[]> classes = new IdentityHashMap<>();
-
-  // Available lazy resource providers.
-  private final List<ResourceProvider> classpathProviders;
-  private final List<ResourceProvider> libraryProviders;
-
-  // Application reader to be used. Note that the reader may be reused in
-  // many loaders and may be used concurrently, it is considered to be
-  // thread-safe since its only state is internal options which we
-  // consider immutable after they are initialized (barring dex factory
-  // which is thread-safe).
-  private final JarApplicationReader reader;
-
-  public LazyClassCollection(JarApplicationReader reader,
-      List<ResourceProvider> classpathProviders, List<ResourceProvider> libraryProviders) {
-    this.classpathProviders = ImmutableList.copyOf(classpathProviders);
-    this.libraryProviders = ImmutableList.copyOf(libraryProviders);
-    this.reader = reader;
-  }
-
-  /**
-   * Returns a definition for a class or `null` if there is no such class.
-   * Parameter `dominator` represents a class that is considered
-   * to be already loaded, it may be null but if specified it may affect
-   * the conflict resolution. For example non-lazy loaded classpath class
-   * provided as `dominator` will conflict with lazy-loaded classpath classes.
-   */
-  public DexClass get(DexType type, DexClass dominator) {
-    DexClass[] candidates;
-    synchronized (classes) {
-      candidates = classes.get(type);
-    }
-
-    if (candidates == null) {
-      String descriptor = type.descriptor.toString();
-
-      // Loading resources and constructing classes may be time consuming, we do it
-      // outside the global lock so others don't have to wait.
-      List<Resource> classpathResources = collectResources(classpathProviders, descriptor);
-      List<Resource> libraryResources = collectResources(libraryProviders, descriptor);
-
-      candidates = new DexClass[classpathResources.size() + libraryResources.size()];
-
-      // Check if someone else has already added the array for this type.
-      synchronized (classes) {
-        DexClass[] existing = classes.get(type);
-        if (existing != null) {
-          assert candidates.length == existing.length;
-          candidates = existing;
-        } else {
-          classes.put(type, candidates);
-        }
-      }
-
-      if (candidates.length > 0) {
-        // Load classes in synchronized content unique for the type.
-        synchronized (candidates) {
-          // Either all or none of the array classes will be loaded, so we use this
-          // as a criteria for checking if we need to load classes.
-          if (candidates[0] == null) {
-            new ClassLoader(type, candidates, reader, classpathResources, libraryResources).load();
-          }
-        }
-      }
-    }
-
-    // Choose class in case there are conflicts.
-    DexClass candidate = dominator;
-    for (DexClass clazz : candidates) {
-      candidate = (candidate == null) ? clazz
-          : DexApplication.chooseClass(candidate, clazz, /* skipLibDups: */ true);
-    }
-    return candidate;
-  }
-
-  private List<Resource> collectResources(List<ResourceProvider> providers, String descriptor) {
-    List<Resource> resources = new ArrayList<>();
-    for (ResourceProvider provider : providers) {
-      Resource resource = provider.getResource(descriptor);
-      if (resource != null) {
-        resources.add(resource);
-      }
-    }
-    return resources;
-  }
-
-  private static final class ClassLoader {
-    int index = 0;
-    final DexType type;
-    final DexClass[] classes;
-    final JarApplicationReader reader;
-    final List<Resource> classpathResources;
-    final List<Resource> libraryResources;
-
-    ClassLoader(DexType type, DexClass[] classes, JarApplicationReader reader,
-        List<Resource> classpathResources, List<Resource> libraryResources) {
-      this.type = type;
-      this.classes = classes;
-      this.reader = reader;
-      this.classpathResources = classpathResources;
-      this.libraryResources = libraryResources;
-    }
-
-    void addClass(DexClass clazz) {
-      assert index < classes.length;
-      assert clazz != null;
-      if (clazz.type != type) {
-        throw new CompilationError("Class content provided for type descriptor "
-            + type.toSourceString() + " actually defines class " + clazz.type
-            .toSourceString());
-      }
-      classes[index++] = clazz;
-    }
-
-    void load() {
-      try (Closer closer = Closer.create()) {
-        for (Resource resource : classpathResources) {
-          JarClassFileReader classReader = new JarClassFileReader(reader, this::addClass);
-          classReader.read(DEFAULT_DEX_FILENAME, ClassKind.CLASSPATH, resource.getStream(closer));
-        }
-        for (Resource resource : libraryResources) {
-          JarClassFileReader classReader = new JarClassFileReader(reader, this::addClass);
-          classReader.read(DEFAULT_DEX_FILENAME, ClassKind.LIBRARY, resource.getStream(closer));
-        }
-      } catch (IOException e) {
-        throw new CompilationError("Failed to load class: " + type.toSourceString(), e);
-      }
-      assert index == classes.length;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
new file mode 100644
index 0000000..e11e660
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LibraryClassCollection.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexLibraryClass;
+import com.android.tools.r8.logging.Log;
+
+/** Represents a collection of library classes. */
+public class LibraryClassCollection extends ClassMap<DexLibraryClass> {
+  public LibraryClassCollection(ClassProvider<DexLibraryClass> classProvider) {
+    super(null, classProvider);
+  }
+
+  @Override
+  DexLibraryClass resolveClassConflict(DexLibraryClass a, DexLibraryClass b) {
+    if (a.origin != Resource.Kind.CLASSFILE || b.origin != Resource.Kind.CLASSFILE) {
+      // We only support conflicts for classes both coming from jar files.
+      throw new CompilationError(
+          "Library type already present: " + a.type.toSourceString());
+    }
+    if (Log.ENABLED) {
+      Log.warn(DexApplication.class,
+          "Class `%s` was specified twice as a library type.", a.type.toSourceString());
+    }
+    return a;
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.LIBRARY;
+  }
+
+  @Override
+  public String toString() {
+    return "library classes: " + super.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
rename to src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
index 9601ff6..07c136d 100644
--- a/src/main/java/com/android/tools/r8/utils/PreloadedResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/PreloadedClassFileProvider.java
@@ -7,9 +7,10 @@
 import static com.android.tools.r8.utils.FileUtils.isArchive;
 import static com.android.tools.r8.utils.FileUtils.isClassFile;
 
+import com.android.tools.r8.ClassFileResourceProvider;
 import com.android.tools.r8.Resource;
-import com.android.tools.r8.ResourceProvider;
 import com.android.tools.r8.errors.CompilationError;
+import com.google.common.collect.Sets;
 import com.google.common.io.ByteStreams;
 
 import java.io.File;
@@ -20,23 +21,25 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 import java.util.zip.ZipInputStream;
 
-/**
- * Lazy resource provider based on preloaded/prebuilt context.
- *
- * NOTE: only handles classfile resources.
- */
-public final class PreloadedResourceProvider implements ResourceProvider {
+/** Lazy Java class file resource provider based on preloaded/prebuilt context. */
+public final class PreloadedClassFileProvider implements ClassFileResourceProvider {
   private final Map<String, byte[]> content;
 
-  private PreloadedResourceProvider(Map<String, byte[]> content) {
+  private PreloadedClassFileProvider(Map<String, byte[]> content) {
     this.content = content;
   }
 
   @Override
+  public Set<String> getClassDescriptors() {
+    return Sets.newHashSet(content.keySet());
+  }
+
+  @Override
   public Resource getResource(String descriptor) {
     byte[] bytes = content.get(descriptor);
     if (bytes == null) {
@@ -46,7 +49,7 @@
   }
 
   /** Create preloaded content resource provider from archive file. */
-  public static ResourceProvider fromArchive(Path archive) throws IOException {
+  public static ClassFileResourceProvider fromArchive(Path archive) throws IOException {
     assert isArchive(archive);
     Builder builder = builder();
     try (ZipInputStream stream = new ZipInputStream(new FileInputStream(archive.toFile()))) {
@@ -85,6 +88,11 @@
     return 'L' + descriptor + ';';
   }
 
+  @Override
+  public String toString() {
+    return content.size() + " preloaded resources";
+  }
+
   /** Create a new empty builder. */
   public static Builder builder() {
     return new Builder();
@@ -105,9 +113,9 @@
       return this;
     }
 
-    public PreloadedResourceProvider build() {
+    public PreloadedClassFileProvider build() {
       assert content != null;
-      PreloadedResourceProvider provider = new PreloadedResourceProvider(content);
+      PreloadedClassFileProvider provider = new PreloadedClassFileProvider(content);
       content = null;
       return provider;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
new file mode 100644
index 0000000..a37eb92
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/ProgramClassCollection.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.Resource;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.desugar.LambdaRewriter;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Represents a collection of library classes. */
+public class ProgramClassCollection extends ClassMap<DexProgramClass> {
+  public static ProgramClassCollection create(List<DexProgramClass> classes) {
+    // We have all classes preloaded, but not necessarily without conflicts.
+    IdentityHashMap<DexType, Value<DexProgramClass>> map = new IdentityHashMap<>();
+    for (DexProgramClass clazz : classes) {
+      Value<DexProgramClass> value = map.get(clazz.type);
+      if (value == null) {
+        value = new Value<>(clazz);
+        map.put(clazz.type, value);
+      } else {
+        value.clazz = resolveClassConflictImpl(value.clazz, clazz);
+      }
+    }
+    return new ProgramClassCollection(map);
+  }
+
+  private ProgramClassCollection(IdentityHashMap<DexType, Value<DexProgramClass>> classes) {
+    super(classes, null);
+  }
+
+  @Override
+  public String toString() {
+    return "program classes: " + super.toString();
+  }
+
+  @Override
+  DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
+    return resolveClassConflictImpl(a, b);
+  }
+
+  @Override
+  ClassKind getClassKind() {
+    return ClassKind.PROGRAM;
+  }
+
+  private static DexProgramClass resolveClassConflictImpl(DexProgramClass a, DexProgramClass b) {
+    // Currently only allow collapsing synthetic lambda classes.
+    if (a.getOrigin() == Resource.Kind.DEX
+        && b.getOrigin() == Resource.Kind.DEX
+        && a.accessFlags.isSynthetic()
+        && b.accessFlags.isSynthetic()
+        && LambdaRewriter.hasLambdaClassPrefix(a.type)
+        && LambdaRewriter.hasLambdaClassPrefix(b.type)) {
+      return a;
+    }
+    throw new CompilationError("Program type already present: " + a.type.toSourceString());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
index 8449bd9..34cd83c 100644
--- a/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8LazyRunExamplesAndroidOTest.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8;
 
-import com.android.tools.r8.utils.DirectoryResourceProvider;
-import com.android.tools.r8.utils.PreloadedResourceProvider;
+import com.android.tools.r8.utils.DirectoryClassFileProvider;
+import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import java.io.IOException;
 import java.nio.file.Path;
 
@@ -25,13 +25,13 @@
 
     private void addClasspathPath(Path location, D8Command.Builder builder) {
       builder.addClasspathResourceProvider(
-          DirectoryResourceProvider.fromDirectory(location.resolve("..")));
+          DirectoryClassFileProvider.fromDirectory(location.resolve("..")));
     }
 
     @Override
     void addLibraryReference(D8Command.Builder builder, Path location) throws IOException {
       builder.addLibraryResourceProvider(
-          PreloadedResourceProvider.fromArchive(location));
+          PreloadedClassFileProvider.fromArchive(location));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/R8EntryPointTests.java b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
new file mode 100644
index 0000000..c83cea6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8EntryPointTests.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class R8EntryPointTests extends TestBase {
+
+  private static final String MAPPING = "mapping.txt";
+  private static final String SEEDS = "seeds.txt";
+  private static final Path INPUT_JAR =
+      Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, "minification" + FileUtils.JAR_EXTENSION);
+  private static final Path PROGUARD_FLAGS =
+      Paths.get(ToolHelper.EXAMPLES_DIR, "minification",  "keep-rules.txt");
+
+  private Path testFlags;
+
+  @Before
+  public void setup() throws IOException {
+    testFlags = temp.newFile("local.flags").toPath();
+    FileUtils.writeTextFile(testFlags, ImmutableList.of(
+        "-printseeds " + SEEDS,
+        "-printmapping " + MAPPING));
+  }
+
+  @Test
+  public void testRun1Dir() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath();
+    R8.run(getCommand(out));
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun1Zip() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    R8.run(getCommand(out));
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun2Dir() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath();
+    ExecutorService executor = Executors.newWorkStealingPool(2);
+    try {
+      R8.run(getCommand(out), executor);
+    } finally {
+      executor.shutdown();
+    }
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testRun2Zip() throws IOException, CompilationException, ProguardRuleParserException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    ExecutorService executor = Executors.newWorkStealingPool(2);
+    try {
+      R8.run(getCommand(out), executor);
+    } finally {
+      executor.shutdown();
+    }
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testMainDir() throws IOException, InterruptedException {
+    Path out = temp.newFolder("outdex").toPath();
+    ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+        "--lib", ToolHelper.getDefaultAndroidJar(),
+        "--output", out.toString(),
+        "--pg-conf", PROGUARD_FLAGS.toString(),
+        "--pg-conf", testFlags.toString(),
+        INPUT_JAR.toString());
+    Assert.assertEquals(0, r8.exitCode);
+    Assert.assertTrue(Files.isRegularFile(out.resolve(FileUtils.DEFAULT_DEX_FILENAME)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  @Test
+  public void testMainZip() throws IOException, InterruptedException {
+    Path out = temp.newFolder("outdex").toPath().resolve("dex.zip");
+    ProcessResult r8 = ToolHelper.forkR8(Paths.get("."),
+        "--lib", ToolHelper.getDefaultAndroidJar(),
+        "--output", out.toString(),
+        "--pg-conf", PROGUARD_FLAGS.toString(),
+        "--pg-conf", testFlags.toString(),
+        INPUT_JAR.toString());
+    Assert.assertEquals(0, r8.exitCode);
+    Assert.assertTrue(Files.isRegularFile(out));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(MAPPING)));
+    Assert.assertTrue(Files.isRegularFile(testFlags.getParent().resolve(SEEDS)));
+  }
+
+  private R8Command getCommand(Path out) throws CompilationException, IOException {
+    return R8Command.builder()
+      .addLibraryFiles(Paths.get(ToolHelper.getDefaultAndroidJar()))
+      .addProgramFiles(INPUT_JAR)
+      .setOutputPath(out)
+      .addProguardConfigurationFiles(PROGUARD_FLAGS, testFlags)
+      .build();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 9be6930..5e265aa 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.JarBuilder;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -40,7 +41,6 @@
 public class R8RunExamplesTest {
 
   private static final String EXAMPLE_DIR = ToolHelper.EXAMPLES_BUILD_DIR;
-  private static final String DEFAULT_DEX_FILENAME = "classes.dex";
 
   // For local testing on a specific Art version(s) change this set. e.g. to
   // ImmutableSet.of(DexVm.ART_DEFAULT) or pass the option -Ddex_vm=<version> to the Java VM.
@@ -161,7 +161,7 @@
   }
 
   private Path getOriginalDexFile() {
-    return Paths.get(EXAMPLE_DIR, pkg, DEFAULT_DEX_FILENAME);
+    return Paths.get(EXAMPLE_DIR, pkg, FileUtils.DEFAULT_DEX_FILENAME);
   }
 
   @Rule
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index ba69bff..08e90c0 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -38,7 +38,7 @@
     }
 
     @Override
-    public int getRegisterForRangedArgument(Value value, int instructionNumber) {
+    public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
       return value.getNumber();
     }
   }
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 dc49caf..d05c800 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -287,7 +287,7 @@
         method.setCode(ir, allocator, factory);
         virtualMethods[i] = method;
       }
-      builder.addClass(
+      builder.addProgramClass(
           new DexProgramClass(
               type,
               null,
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index f5b2b1a..8f46572 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -11,13 +11,16 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.errors.CompilationError;
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
@@ -95,7 +98,11 @@
     actualMapping = new String(Files.readAllBytes(outputmapping), StandardCharsets.UTF_8);
     String refMapping = new String(Files.readAllBytes(
         Paths.get(EXAMPLES_DIR, "shaking1", "print-mapping.ref")), StandardCharsets.UTF_8);
-    Assert.assertEquals(refMapping, actualMapping);
+    Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
   }
 
+  private static String sorted(String str) {
+    return new BufferedReader(new StringReader(str))
+        .lines().sorted().collect(Collectors.joining("\n"));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index 5e67ad1..ca30f76 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -93,7 +93,7 @@
             assertTrue(
                 defBlock.getInstructions().get(defBlock.getInstructions().size() - 2).isInvoke());
             for (BasicBlock returnPredecessor : block.getPredecessors()) {
-              if (defBlock.isCatchSuccessor(returnPredecessor)) {
+              if (defBlock.hasCatchSuccessor(returnPredecessor)) {
                 hasExceptionalPredecessor = true;
               } else if (defBlock == returnPredecessor) {
                 // Normal flow goes to return.
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index 966f786..480774f 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -8,6 +8,7 @@
 import r8
 import d8
 import sys
+import utils
 
 import gmscore_data
 import youtube_data
@@ -49,8 +50,12 @@
                     help='')
   result.add_option('-k',
                     help='Override the default ProGuard keep rules')
+  result.add_option('--compiler-flags',
+                    help='Additional option(s) for the compiler. ' +
+                         'If passing several options use a quoted string.')
   result.add_option('--r8-flags',
-                    help='Additional option(s) for R8. ' +
+                    help='Additional option(s) for the compiler. ' +
+                         'Same as --compiler-flags, keeping it for backward compatibility. ' +
                          'If passing several options use a quoted string.')
   result.add_option('--track-memory-to-file',
                     help='Track how much memory the jvm is using while ' +
@@ -65,6 +70,17 @@
                     'the run.')
   return result.parse_args()
 
+# Most apps have the -printmapping and -printseeds in the Proguard
+# configuration. However we don't want to write these files in these
+# locations. Instead generate an auxiliary Proguard configuration
+# placing these two output files together with the dex output.
+def GenerateAdditionalProguardConfiguration(temp, outdir):
+  name = "output.config"
+  with open(os.path.join(temp, name), 'w') as file:
+    file.write('-printmapping ' + os.path.join(outdir, 'proguard.map') + "\n")
+    file.write('-printseeds ' + os.path.join(outdir, 'proguard.seeds') + "\n")
+    return os.path.abspath(file.name)
+
 def main():
   (options, args) = ParseOptions()
   outdir = options.out
@@ -119,8 +135,11 @@
   if options.compiler == 'r8':
     if 'r8-flags' in values:
       args.extend(values['r8-flags'].split(' '))
-    if options.r8_flags:
-      args.extend(options.r8_flags.split(' '))
+
+  if options.compiler_flags:
+    args.extend(options.compiler_flags.split(' '))
+  if options.r8_flags:
+    args.extend(options.r8_flags.split(' '))
 
   if inputs:
     args.extend(inputs)
@@ -133,8 +152,16 @@
       d8.run(args, not options.no_build, not options.no_debug, options.profile,
              options.track_memory_to_file)
     else:
-      r8.run(args, not options.no_build, not options.no_debug, options.profile,
-             options.track_memory_to_file)
+      with utils.TempDir() as temp:
+        if outdir.endswith('.zip') or outdir.endswith('.jar'):
+          pg_outdir = os.path.dirname(outdir)
+        else:
+          pg_outdir = outdir
+        additional_pg_conf = GenerateAdditionalProguardConfiguration(
+            temp, os.path.abspath(pg_outdir))
+        args.extend(['--pg-conf', additional_pg_conf])
+        r8.run(args, not options.no_build, not options.no_debug, options.profile,
+               options.track_memory_to_file)
 
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/tools/windows/README.dx b/tools/windows/README.dx
index 99db256..348dc4a 100644
--- a/tools/windows/README.dx
+++ b/tools/windows/README.dx
@@ -21,3 +21,5 @@
 ---
 > call java %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
 
+dexmerger.bat has been copied from dx.bat, and the command line has been updated
+according to the SDK dexmerger bash script to call the right class.
\ No newline at end of file
diff --git a/tools/windows/dx.tar.gz.sha1 b/tools/windows/dx.tar.gz.sha1
index beadad3..0f3e345 100644
--- a/tools/windows/dx.tar.gz.sha1
+++ b/tools/windows/dx.tar.gz.sha1
@@ -1 +1 @@
-1d680c9efc9e17d4fc51e8afd0bbe1b3d8724903
\ No newline at end of file
+9adeae753e17fa0a663e4d458b406a39ded27621
\ No newline at end of file