Merge "Use gson instead of json_simple."
diff --git a/build.gradle b/build.gradle
index 6db08fe..8ee6b41 100644
--- a/build.gradle
+++ b/build.gradle
@@ -514,7 +514,7 @@
     classifier = null
     version = null
     manifest {
-        attributes 'Main-Class': 'com.android.tools.r8.R8'
+        attributes 'Main-Class': 'com.android.tools.r8.SwissArmyKnife'
     }
     // In order to build without dependencies, pass the exclude_deps property using:
     // gradle -Pexclude_deps R8
@@ -527,164 +527,27 @@
 }
 
 task D8(type: ShadowJar) {
-    from consolidatedLicense.outputs.files
-    exclude { path ->
-      path.getRelativePath().getPathString().startsWith("META-INF")
-    }
+    from R8.outputs.files
     baseName 'd8'
-    classifier = null
-    version = null
     manifest {
         attributes 'Main-Class': 'com.android.tools.r8.D8'
     }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps D8
-    if (!project.hasProperty('exclude_deps')) {
-        from repackageSources.outputs.files
-        from repackageDeps.outputs.files
-    } else {
-        from sourceSets.main.output
-    }
 }
 
-task CompatDx(type: Jar) {
-    from sourceSets.main.output
+task CompatDx(type: ShadowJar) {
+    from R8.outputs.files
     baseName 'compatdx'
     manifest {
       attributes 'Main-Class': 'com.android.tools.r8.compatdx.CompatDx'
     }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps CompatDx
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
 }
 
-task DexFileMerger(type: Jar) {
-    from sourceSets.main.output
-    baseName 'dexfilemerger'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.dexfilemerger.DexFileMerger'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps CompatDx
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task DexSplitter(type: Jar) {
-    from sourceSets.main.output
-    baseName 'dexsplitter'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.dexsplitter.DexSplitter'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps CompatDx
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task CompatProguard(type: Jar) {
-    from sourceSets.main.output
+task CompatProguard(type: ShadowJar) {
+    from R8.outputs.files
     baseName 'compatproguard'
     manifest {
         attributes 'Main-Class': 'com.android.tools.r8.compatproguard.CompatProguard'
     }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps CompatProguard
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task D8Logger(type: Jar) {
-    from sourceSets.main.output
-    baseName 'd8logger'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.D8Logger'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps D8Logger
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task disasm(type: Jar) {
-    from sourceSets.main.output
-    baseName 'disasm'
-    manifest {
-        attributes 'Main-Class': 'com.android.tools.r8.Disassemble'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps D8
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task bisect(type: Jar) {
-    from sourceSets.main.output
-    baseName 'bisect'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.bisect.Bisect'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps R8
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task DexSegments(type: Jar) {
-    from sourceSets.main.output
-    baseName 'dexsegments'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.DexSegments'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps DexSegments
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task maindex(type: Jar) {
-    from sourceSets.main.output
-    baseName 'maindex'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.GenerateMainDexList'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps maindex
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
-}
-
-task ExtractMarker(type: Jar) {
-    from sourceSets.main.output
-    baseName 'extractmarker'
-    manifest {
-      attributes 'Main-Class': 'com.android.tools.r8.ExtractMarker'
-    }
-    // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps ExtractMarker
-    if (!project.hasProperty('exclude_deps')) {
-        // Also include dependencies
-        from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
-    }
 }
 
 task bspatch(type: Jar) {
@@ -694,7 +557,7 @@
         attributes 'Main-Class': 'com.android.tools.r8.dex.BSPatch'
     }
     // In order to build without dependencies, pass the exclude_deps property using:
-    // gradle -Pexclude_deps maindex
+    // gradle -Pexclude_deps bspatch
     if (!project.hasProperty('exclude_deps')) {
         // Also include dependencies
         from {
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index aa99407..2d52c64 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -155,6 +155,7 @@
       // Disable global optimizations.
       options.enableMinification = false;
       options.enableInlining = false;
+      options.enableClassInlining = false;
       options.outline.enabled = false;
 
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index bb7c37f..4c65134 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -333,6 +333,8 @@
     // Disable some of R8 optimizations.
     assert internal.enableInlining;
     internal.enableInlining = false;
+    assert internal.enableClassInlining;
+    internal.enableClassInlining = false;
     assert internal.enableSwitchMapRemoval;
     internal.enableSwitchMapRemoval = false;
     assert internal.outline.enabled;
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index 3db9699..26097c1 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -59,7 +59,7 @@
     // Print -whyareyoukeeping results if any.
     if (mainDexRootSet.reasonAsked.size() > 0) {
       // Print reasons on the application after pruning, so that we reflect the actual result.
-      TreePruner pruner = new TreePruner(application, appInfo.withLiveness(), options);
+      TreePruner pruner = new TreePruner(application, mainDexAppInfo.withLiveness(), options);
       application = pruner.run();
       ReasonPrinter reasonPrinter = enqueuer.getReasonPrinter(mainDexRootSet.reasonAsked);
       reasonPrinter.run(application);
diff --git a/src/main/java/com/android/tools/r8/JarDiff.java b/src/main/java/com/android/tools/r8/JarDiff.java
new file mode 100644
index 0000000..dad57d4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/JarDiff.java
@@ -0,0 +1,260 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static com.google.common.io.ByteStreams.toByteArray;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.graph.ClassKind;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+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.origin.PathOrigin;
+import com.android.tools.r8.utils.InternalOptions;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Command-line program to compare two JARs. Given two JARs as input, the program first outputs a
+ * list of classes only in one of the input JARs. Then, for each common class, the program outputs a
+ * list of methods only in one of the input JARs. For each common method, the output of
+ * CfInstruction.toString() on each instruction is compared to find instruction-level differences. A
+ * simple diffing algorithm is used that simply removes the common prefix and common suffix and
+ * prints everything from the first difference to the last difference in the method code.
+ */
+public class JarDiff {
+
+  private static final String USAGE =
+      "Arguments: <input1.jar> <input2.jar>\n"
+          + "\n"
+          + "JarDiff computes the difference between two JAR files that contain Java classes.\n"
+          + "\n"
+          + "Only method codes are compared. Fields, parameters, annotations, generic\n"
+          + "signatures etc. are ignored.\n"
+          + "\n"
+          + "Note: Jump targets are ignored, so if two methods differ only in what label an\n"
+          + "IF or GOTO instruction jumps to, no difference is output.";
+
+  public static void main(String[] args) throws Exception {
+    JarDiff jarDiff = JarDiff.parse(args);
+    if (jarDiff == null) {
+      System.out.println(USAGE);
+    } else {
+      jarDiff.run();
+    }
+  }
+
+  public static JarDiff parse(String[] args) {
+    int arg = 0;
+    int before = 3;
+    int after = 3;
+    while (arg + 1 < args.length) {
+      if (args[arg].equals("-B")) {
+        before = Integer.parseInt(args[arg + 1]);
+        arg += 2;
+      } else if (args[arg].equals("-A")) {
+        after = Integer.parseInt(args[arg + 1]);
+        arg += 2;
+      } else {
+        break;
+      }
+    }
+    if (args.length != arg + 2) {
+      return null;
+    }
+    return new JarDiff(Paths.get(args[arg]), Paths.get(args[arg + 1]), before, after);
+  }
+
+  private final Path path1;
+  private final Path path2;
+  private final int before;
+  private final int after;
+  private final JarApplicationReader applicationReader;
+  private ArchiveClassFileProvider archive1;
+  private ArchiveClassFileProvider archive2;
+
+  public JarDiff(Path path1, Path path2, int before, int after) {
+    this.path1 = path1;
+    this.path2 = path2;
+    this.before = before;
+    this.after = after;
+    InternalOptions options = new InternalOptions();
+    options.enableCfFrontend = true;
+    applicationReader = new JarApplicationReader(options);
+  }
+
+  public void run() throws Exception {
+    archive1 = new ArchiveClassFileProvider(path1);
+    archive2 = new ArchiveClassFileProvider(path2);
+    for (String descriptor : getCommonDescriptors()) {
+      byte[] bytes1 = getClassAsBytes(archive1, descriptor);
+      byte[] bytes2 = getClassAsBytes(archive2, descriptor);
+      if (Arrays.equals(bytes1, bytes2)) {
+        continue;
+      }
+      DexProgramClass class1 = getDexProgramClass(path1, bytes1);
+      DexProgramClass class2 = getDexProgramClass(path2, bytes2);
+      compareMethods(class1, class2);
+    }
+  }
+
+  private List<String> getCommonDescriptors() {
+    List<String> descriptors1 = getSortedDescriptorList(archive1);
+    List<String> descriptors2 = getSortedDescriptorList(archive2);
+    if (descriptors1.equals(descriptors2)) {
+      return descriptors1;
+    }
+    List<String> only1 = setMinus(descriptors1, descriptors2);
+    List<String> only2 = setMinus(descriptors2, descriptors1);
+    if (!only1.isEmpty()) {
+      System.out.println("Only in " + path1 + ": " + only1);
+    }
+    if (!only2.isEmpty()) {
+      System.out.println("Only in " + path2 + ": " + only2);
+    }
+    return setIntersection(descriptors1, descriptors2);
+  }
+
+  private List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
+    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
+    Collections.sort(descriptorList);
+    return descriptorList;
+  }
+
+  private byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
+      throws Exception {
+    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
+  }
+
+  private DexProgramClass getDexProgramClass(Path path, byte[] bytes) throws IOException {
+
+    class Collector implements Consumer<DexClass> {
+
+      private DexClass dexClass;
+
+      @Override
+      public void accept(DexClass dexClass) {
+        this.dexClass = dexClass;
+      }
+
+      public DexClass get() {
+        assert dexClass != null;
+        return dexClass;
+      }
+    }
+
+    Collector collector = new Collector();
+    JarClassFileReader reader = new JarClassFileReader(applicationReader, collector);
+    reader.read(new PathOrigin(path), ClassKind.PROGRAM, new ByteArrayInputStream(bytes));
+    return collector.get().asProgramClass();
+  }
+
+  private void compareMethods(DexProgramClass class1, DexProgramClass class2) {
+    class1.forEachMethod(
+        method1 -> {
+          DexEncodedMethod method2 = class2.lookupMethod(method1.method);
+          if (method2 == null) {
+            compareMethods(method1, method2);
+          }
+        });
+    class2.forEachMethod(
+        method2 -> {
+          DexEncodedMethod method1 = class1.lookupMethod(method2.method);
+          compareMethods(method1, method2);
+        });
+  }
+
+  private void compareMethods(DexEncodedMethod m1, DexEncodedMethod m2) {
+    if (m1 == null) {
+      System.out.println("Only in " + path2 + ": " + m2.method.toSourceString());
+      return;
+    }
+    if (m2 == null) {
+      System.out.println("Only in " + path1 + ": " + m1.method.toSourceString());
+      return;
+    }
+    List<String> code1 = getInstructionStrings(m1);
+    List<String> code2 = getInstructionStrings(m2);
+    if (code1.equals(code2)) {
+      return;
+    }
+    int i = getCommonPrefix(code1, code2);
+    int j = getCommonSuffix(code1, code2);
+    int length1 = code1.size() - i - j;
+    int length2 = code2.size() - i - j;
+    int before = Math.min(i, this.before);
+    int after = Math.min(j, this.after);
+    int context = before + after;
+    System.out.println("--- " + path1 + "/" + m1.method.toSmaliString());
+    System.out.println("+++ " + path2 + "/" + m2.method.toSmaliString());
+    System.out.println(
+        "@@ -" + (i - before) + "," + (length1 + context)
+            + " +" + (i - before) + "," + (length2 + context) + " @@ "
+            + m1.method.toSourceString());
+    for (int k = 0; k < before; k++) {
+      System.out.println(" " + code1.get(i - before + k));
+    }
+    for (int k = 0; k < length1; k++) {
+      System.out.println("-" + code1.get(i + k));
+    }
+    for (int k = 0; k < length2; k++) {
+      System.out.println("+" + code2.get(i + k));
+    }
+    for (int k = 0; k < after; k++) {
+      System.out.println(" " + code1.get(i + length1 + k));
+    }
+  }
+
+  private static List<String> getInstructionStrings(DexEncodedMethod method) {
+    List<CfInstruction> instructions = method.getCode().asCfCode().getInstructions();
+    return instructions.stream().map(CfInstruction::toString).collect(Collectors.toList());
+  }
+
+  private static List<String> setIntersection(List<String> set1, List<String> set2) {
+    ArrayList<String> result = new ArrayList<>(set1);
+    result.retainAll(new HashSet<>(set2));
+    return result;
+  }
+
+  private static List<String> setMinus(List<String> set, List<String> toRemove) {
+    ArrayList<String> result = new ArrayList<>(set);
+    result.removeAll(new HashSet<>(toRemove));
+    return result;
+  }
+
+  private static int getCommonPrefix(List<String> code1, List<String> code2) {
+    int i = 0;
+    while (i < code1.size() && i < code2.size()) {
+      if (code1.get(i).equals(code2.get(i))) {
+        i++;
+      } else {
+        break;
+      }
+    }
+    return i;
+  }
+
+  private static int getCommonSuffix(List<String> code1, List<String> code2) {
+    int j = 0;
+    while (j < code1.size() && j < code2.size()) {
+      if (code1.get(code1.size() - j - 1).equals(code2.get(code2.size() - j - 1))) {
+        j++;
+      } else {
+        break;
+      }
+    }
+    return j;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6a1d7c8..53e2f87 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -358,12 +358,9 @@
         }
         application = application.asDirect().rewrittenWithLense(graphLense);
         appInfo = appInfo.withLiveness().rewrittenWithLense(application.asDirect(), graphLense);
-        // TODO(mathiasr): Remove this check when CF->IR construction is complete.
-        if (!options.skipIR) {
-          // Collect switch maps and ordinals maps.
-          appInfo = new SwitchMapCollector(appInfo.withLiveness(), options).run();
-          appInfo = new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
-        }
+        // Collect switch maps and ordinals maps.
+        appInfo = new SwitchMapCollector(appInfo.withLiveness(), options).run();
+        appInfo = new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
 
         // TODO(b/79143143): re-enable once fixed.
         // graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withLiveness()).run();
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index a52d694..dafa35a 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -12,9 +12,11 @@
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceBytes;
 import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
 import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
@@ -22,15 +24,17 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
- * Immutable command structure for an invocation of the {@link D8} compiler.
+ * Immutable command structure for an invocation of the {@link R8} compiler.
  *
  * <p>To build a R8 command use the {@link R8Command.Builder} class. For example:
  *
@@ -233,7 +237,7 @@
         Path path,
         OutputMode mode,
         boolean consumeDataResources) {
-      return super.createProgramOutputConsumer(path, mode, true);
+      return super.createProgramOutputConsumer(path, mode, false);
     }
 
     @Override
@@ -282,21 +286,54 @@
         mainDexKeepRules = parser.getConfig().getRules();
       }
 
-      ProguardConfiguration.Builder configurationBuilder;
-      if (proguardConfigs.isEmpty()) {
-        configurationBuilder = ProguardConfiguration.builder(factory, reporter);
-      } else {
-        ProguardConfigurationParser parser =
-            new ProguardConfigurationParser(factory, reporter);
+      ProguardConfigurationParser parser = new ProguardConfigurationParser(factory, reporter);
+      if (!proguardConfigs.isEmpty()) {
         parser.parse(proguardConfigs);
-        configurationBuilder = parser.getConfigurationBuilder();
-        configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
       }
+      ProguardConfiguration.Builder configurationBuilder = parser.getConfigurationBuilder();
+      configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
 
       if (proguardConfigurationConsumer != null) {
         proguardConfigurationConsumer.accept(configurationBuilder);
       }
 
+      // Process Proguard configurations supplied through data resources in the input.
+      DataResourceProvider.Visitor embeddedProguardConfigurationVisitor =
+          new DataResourceProvider.Visitor() {
+            @Override
+            public void visit(DataDirectoryResource directory) {
+              // Don't do anything.
+            }
+
+            @Override
+            public void visit(DataEntryResource resource) {
+              if (resource.getName().startsWith("META-INF/proguard/")) {
+                try (InputStream in = resource.getByteStream()) {
+                  ProguardConfigurationSource source =
+                      new ProguardConfigurationSourceBytes(in, resource.getOrigin());
+                  parser.parse(source);
+                } catch (ResourceException e) {
+                  reporter.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
+                      resource.getOrigin()));
+                } catch (Exception e) {
+                  reporter.error(new ExceptionDiagnostic(e, resource.getOrigin()));
+                }
+              }
+            }
+          };
+
+      getAppBuilder().getProgramResourceProviders()
+          .stream()
+          .map(ProgramResourceProvider::getDataResourceProvider)
+          .filter(Objects::nonNull)
+          .forEach(dataResourceProvider -> {
+              try {
+                dataResourceProvider.accept(embeddedProguardConfigurationVisitor);
+              } catch (ResourceException e) {
+                reporter.error(new ExceptionDiagnostic(e));
+              }
+          });
+
       if (disableTreeShaking) {
         configurationBuilder.disableShrinking();
       }
@@ -369,6 +406,11 @@
       }
       return resources;
     }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return provider.getDataResourceProvider();
+    }
   }
 
   // Internal state to verify parsing properties not enforced by the builder.
@@ -659,6 +701,7 @@
     if (internal.debug) {
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
+      internal.enableClassInlining = false;
       // TODO(zerny): Should we support outlining in debug mode? b/62937285
       internal.outline.enabled = false;
     }
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
new file mode 100644
index 0000000..0e35847
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.bisect.Bisect;
+import com.android.tools.r8.compatdx.CompatDx;
+import com.android.tools.r8.compatproguard.CompatProguard;
+import com.android.tools.r8.dexfilemerger.DexFileMerger;
+import com.android.tools.r8.dexsplitter.DexSplitter;
+import java.util.Arrays;
+
+/**
+ * Common entry point to everything in the R8 project.
+ *
+ * <p>This class is used as the main class in {@code r8.jar}. It checks the first command-line
+ * argument to find the tool to run, or runs {@link R8} if the first argument is not a recognized
+ * tool name.
+ *
+ * <p>The set of tools recognized by this class is defined by a switch statement in {@link
+ * SwissArmyKnife#main(String[])}.
+ */
+public class SwissArmyKnife {
+
+  public static void main(String[] args) throws Exception {
+    if (args.length == 0) {
+      runDefault(args);
+      return;
+    }
+    switch (args[0]) {
+      case "r8":
+        R8.main(shift(args));
+        break;
+      case "d8":
+        D8.main(shift(args));
+        break;
+      case "compatdx":
+        CompatDx.main(shift(args));
+        break;
+      case "dexfilemerger":
+        DexFileMerger.main(shift(args));
+        break;
+      case "dexsplitter":
+        DexSplitter.main(shift(args));
+        break;
+      case "compatproguard":
+        CompatProguard.main(shift(args));
+        break;
+      case "d8logger":
+        D8Logger.main(shift(args));
+        break;
+      case "disasm":
+        Disassemble.main(shift(args));
+        break;
+      case "bisect":
+        Bisect.main(shift(args));
+        break;
+      case "dexsegments":
+        DexSegments.main(shift(args));
+        break;
+      case "maindex":
+        GenerateMainDexList.main(shift(args));
+        break;
+      case "extractmarker":
+        ExtractMarker.main(shift(args));
+        break;
+      case "jardiff":
+        JarDiff.main(shift(args));
+        break;
+      default:
+        runDefault(args);
+        break;
+    }
+  }
+
+  private static void runDefault(String[] args) {
+    R8.main(args);
+  }
+
+  private static String[] shift(String[] args) {
+    return Arrays.copyOfRange(args, 1, args.length);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 124bccc..98ed5e9 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.2.16-dev";
+  public static final String LABEL = "1.2.17-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 6d7ae19..f907ffb 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -50,6 +50,7 @@
 import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.If;
@@ -124,9 +125,10 @@
       for (int i = 0; i < tryCatch.guards.size(); i++) {
         newline();
         DexType guard = tryCatch.guards.get(i);
+        assert guard != null;
         builder
             .append(".catch ")
-            .append(guard == null ? "all" : guard.getInternalName())
+            .append(guard == DexItemFactory.catchAllType ? "all" : guard.getInternalName())
             .append(" from ")
             .append(getLabel(tryCatch.start))
             .append(" to ")
@@ -430,7 +432,7 @@
     Kind kind = cfSwitch.getKind();
     builder.append(kind == Kind.LOOKUP ? "lookup" : "table").append("switch");
     IntList keys = cfSwitch.getKeys();
-    List<CfLabel> targets = cfSwitch.getTargets();
+    List<CfLabel> targets = cfSwitch.getSwitchTargets();
     for (int i = 0; i < targets.size(); i++) {
       indent();
       int key = kind == Kind.LOOKUP ? keys.getInt(i) : (keys.getInt(0) + i);
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
index 423bba2..842cbe9 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -116,4 +120,36 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitInsn(getAsmOpcode());
   }
+
+  @Override
+  public boolean canThrow() {
+    return (type != NumericType.FLOAT && type != NumericType.DOUBLE)
+        && (opcode == Opcode.Div || opcode == Opcode.Rem);
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int right = state.pop().register;
+    int left = state.pop().register;
+    int dest = state.push(ValueType.fromNumericType(type)).register;
+    switch (opcode) {
+      case Add:
+        builder.addAdd(type, dest, left, right);
+        break;
+      case Sub:
+        builder.addSub(type, dest, left, right);
+        break;
+      case Mul:
+        builder.addMul(type, dest, left, right);
+        break;
+      case Div:
+        builder.addDiv(type, dest, left, right);
+        break;
+      case Rem:
+        builder.addRem(type, dest, left, right);
+        break;
+      default:
+        throw new Unreachable("CfArithmeticBinop has unknown opcode " + opcode);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
index dfe7c1b..8fb77c1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -19,4 +23,16 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot array = state.pop();
+    assert array.type.isObjectOrNull();
+    builder.addArrayLength(state.push(builder.getFactory().intType).register, array.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
index 85405e4..c0b1a07 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
@@ -6,6 +6,11 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -55,4 +60,25 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot index = state.pop();
+    Slot array = state.pop();
+    Slot value;
+    assert array.type.isObjectOrNull();
+    ValueType memberType = ValueType.fromMemberType(type);
+    if (array.preciseType != null) {
+      value = state.push(array.preciseType.toArrayElementType(builder.getFactory()));
+      assert state.peek().type == memberType;
+    } else {
+      value = state.push(memberType);
+    }
+    builder.addArrayGet(type, value.register, array.register, index.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
index 4b13af4..ff76777 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -55,4 +59,17 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot value = state.pop();
+    Slot index = state.pop();
+    Slot array = state.pop();
+    builder.addArrayPut(type, value.register, array.register, index.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
index e46d475..c01d6d6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -36,4 +40,17 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerCheckCast(type);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    // Pop the top value and push it back on with the checked type.
+    state.pop();
+    Slot object = state.push(type);
+    builder.addCheckCast(object.register, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
index 9c6d3d7..b96ca89 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
@@ -8,6 +8,10 @@
 import com.android.tools.r8.ir.code.Cmp;
 import com.android.tools.r8.ir.code.Cmp.Bias;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -66,4 +70,11 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitInsn(getAsmOpcode());
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int right = state.pop().register;
+    int left = state.pop().register;
+    builder.addCmp(type, bias, state.push(ValueType.fromNumericType(type)).register, left, right);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
index e4037e4..4da0b5c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
@@ -7,6 +7,9 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Type;
@@ -33,6 +36,11 @@
     printer.print(this);
   }
 
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
   private String getInternalName(NamingLens lens) {
     switch (type.toShorty()) {
       case '[':
@@ -63,4 +71,9 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerConstClass(type);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addConstClass(state.push(builder.getFactory().classType).register, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
index ca34be7..0a98cb3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
@@ -3,10 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -36,4 +40,17 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerMethodHandle(handle);
   }
+
+  @Override
+  public boolean canThrow() {
+    // const-class and const-string* may throw in dex.
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code)
+      throws ApiLevelException {
+    builder.addConstMethodHandle(
+        state.push(builder.getFactory().methodHandleType).register, handle);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
index 7e06a88..e933c11 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
@@ -3,10 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Type;
@@ -37,4 +41,16 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerProto(type);
   }
+
+  @Override
+  public boolean canThrow() {
+    // const-class and const-string* may throw in dex.
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code)
+      throws ApiLevelException {
+    builder.addConstMethodType(state.push(builder.getFactory().methodTypeType).register, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
index 385669d..e72c9e1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -19,4 +23,9 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addNullConst(state.push(ValueType.OBJECT).register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
index f3aa7f3..c24490c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
@@ -6,6 +6,9 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -118,4 +121,9 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addConst(type, state.push(type).register, value);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
index a22f279..54ae1c6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
@@ -5,6 +5,9 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -29,4 +32,15 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    // const-class and const-string* may throw in dex.
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addConstString(state.push(builder.getFactory().stringType).register, string);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index 130362f..350c1c9 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -8,6 +8,10 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -65,4 +69,42 @@
         throw new Unreachable("Unexpected opcode " + opcode);
     }
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    DexType type = field.type;
+    switch (opcode) {
+      case Opcodes.GETSTATIC:
+        {
+          builder.addStaticGet(state.push(type).register, field);
+          break;
+        }
+      case Opcodes.PUTSTATIC:
+        {
+          Slot value = state.pop();
+          builder.addStaticPut(value.register, field);
+          break;
+        }
+      case Opcodes.GETFIELD:
+        {
+          Slot object = state.pop();
+          builder.addInstanceGet(state.push(type).register, object.register, field);
+          break;
+        }
+      case Opcodes.PUTFIELD:
+        {
+          Slot value = state.pop();
+          Slot object = state.pop();
+          builder.addInstancePut(value.register, object.register, field);
+          break;
+        }
+      default:
+        throw new Unreachable("Unexpected opcode " + opcode);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index d015de1..6e46028 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -9,6 +9,9 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.util.List;
@@ -50,6 +53,22 @@
       return null;
     }
 
+    public boolean isUninitializedThis() {
+      return false;
+    }
+
+    public boolean isInitialized() {
+      return false;
+    }
+
+    public DexType getInitializedType() {
+      return null;
+    }
+
+    public boolean isTop() {
+      return false;
+    }
+
     private FrameType() {}
   }
 
@@ -92,6 +111,16 @@
     public boolean isWide() {
       return type.isPrimitiveType() && (type.toShorty() == 'J' || type.toShorty() == 'D');
     }
+
+    @Override
+    public boolean isInitialized() {
+      return true;
+    }
+
+    @Override
+    public DexType getInitializedType() {
+      return type;
+    }
   }
 
   private static class Top extends FrameType {
@@ -107,6 +136,11 @@
     Object getTypeOpcode(NamingLens lens) {
       return Opcodes.TOP;
     }
+
+    @Override
+    public boolean isTop() {
+      return true;
+    }
   }
 
   private static class UninitializedNew extends FrameType {
@@ -127,6 +161,11 @@
     }
 
     @Override
+    public boolean isUninitializedNew() {
+      return true;
+    }
+
+    @Override
     public CfLabel getUninitializedLabel() {
       return label;
     }
@@ -144,6 +183,11 @@
     public String toString() {
       return "uninitialized this";
     }
+
+    @Override
+    public boolean isUninitializedThis() {
+      return true;
+    }
   }
 
   private final Int2ReferenceSortedMap<FrameType> locals;
@@ -232,4 +276,15 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    // TODO(mathiasr): Verify stack map frames before building IR.
+    code.setStateFromFrame(this);
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
index 47cfed6..c4975d8 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
@@ -4,6 +4,9 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -16,6 +19,7 @@
     this.target = target;
   }
 
+  @Override
   public CfLabel getTarget() {
     return target;
   }
@@ -29,4 +33,9 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addGoto(code.getLabelOffset(target));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIf.java b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
index d39c358..c97f3b6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
@@ -8,6 +8,9 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -32,6 +35,7 @@
     return kind;
   }
 
+  @Override
   public CfLabel getTarget() {
     return target;
   }
@@ -64,4 +68,17 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitJumpInsn(getOpcode(), target.getLabel());
   }
+
+  @Override
+  public boolean isConditionalJump() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int value = state.pop().register;
+    int trueTargetOffset = code.getLabelOffset(target);
+    int falseTargetOffset = code.getCurrentInstructionIndex() + 1;
+    builder.addIfZero(kind, type, value, trueTargetOffset, falseTargetOffset);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
index 2a076e4..26410c8 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
@@ -8,6 +8,9 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -32,6 +35,7 @@
     return type;
   }
 
+  @Override
   public CfLabel getTarget() {
     return target;
   }
@@ -64,4 +68,18 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitJumpInsn(getOpcode(), target.getLabel());
   }
+
+  @Override
+  public boolean isConditionalJump() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int right = state.pop().register;
+    int left = state.pop().register;
+    int trueTargetOffset = code.getLabelOffset(target);
+    int falseTargetOffset = code.getCurrentInstructionIndex() + 1;
+    builder.addIf(kind, type, left, right, trueTargetOffset, falseTargetOffset);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
index 6db7250..20b3928 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -34,4 +38,10 @@
   public int getIncrement() {
     return increment;
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int local = state.read(var).register;
+    builder.addAddLiteral(NumericType.INT, local, local, increment);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
index deba4ad..6a093df 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
@@ -6,6 +6,9 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -36,4 +39,15 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerTypeReference(type);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int value = state.pop().register;
+    builder.addInstanceOf(state.push(builder.getFactory().booleanType).register, value, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
index 04ab380..eaa7b2a 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
@@ -3,9 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -25,4 +29,31 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     // Intentionally empty.
   }
+
+  public CfLabel getTarget() {
+    return null;
+  }
+
+  /** Return true if this instruction is CfReturn or CfReturnVoid. */
+  public boolean isReturn() {
+    return false;
+  }
+
+  /** Return true if this instruction is CfIf or CfIfCmp. */
+  public boolean isConditionalJump() {
+    return false;
+  }
+
+  /** Return true if this instruction or its DEX equivalent can throw. */
+  public boolean canThrow() {
+    return false;
+  }
+
+  public abstract void buildIR(IRBuilder builder, CfState state, CfSourceCode code)
+      throws ApiLevelException;
+
+  /** Return true if this instruction directly emits IR instructions. */
+  public boolean emitsIR() {
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 82f2438..32c7022 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -3,13 +3,23 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
+import java.util.Arrays;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
@@ -74,4 +84,73 @@
         throw new Unreachable("unknown CfInvoke opcode " + opcode);
     }
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code)
+      throws ApiLevelException {
+    Invoke.Type type;
+    DexMethod canonicalMethod;
+    DexProto callSiteProto = null;
+    switch (opcode) {
+      case Opcodes.INVOKEINTERFACE:
+        {
+          canonicalMethod = method;
+          type = Type.INTERFACE;
+          break;
+        }
+      case Opcodes.INVOKEVIRTUAL:
+        {
+          canonicalMethod = builder.getFactory().polymorphicMethods.canonicalize(method);
+          if (canonicalMethod == null) {
+            type = Type.VIRTUAL;
+            canonicalMethod = method;
+          } else {
+            type = Type.POLYMORPHIC;
+            callSiteProto = method.proto;
+          }
+          break;
+        }
+      case Opcodes.INVOKESPECIAL:
+        {
+          canonicalMethod = method;
+          if (method.name.toString().equals(Constants.INSTANCE_INITIALIZER_NAME)) {
+            type = Type.DIRECT;
+          } else if (builder.getMethod().holder == method.holder) {
+            type = Type.DIRECT;
+          } else {
+            type = Type.SUPER;
+          }
+          break;
+        }
+      case Opcodes.INVOKESTATIC:
+        {
+          canonicalMethod = method;
+          type = Type.STATIC;
+          break;
+        }
+      default:
+        throw new Unreachable("unknown CfInvoke opcode " + opcode);
+    }
+    int parameterCount = method.proto.parameters.size();
+    if (type != Type.STATIC) {
+      parameterCount += 1;
+    }
+    ValueType[] types = new ValueType[parameterCount];
+    Integer[] registers = new Integer[parameterCount];
+    for (int i = parameterCount - 1; i >= 0; i--) {
+      Slot slot = state.pop();
+      types[i] = slot.type;
+      registers[i] = slot.register;
+    }
+    builder.addInvoke(
+        type, canonicalMethod, callSiteProto, Arrays.asList(types), Arrays.asList(registers), itf);
+    if (!method.proto.returnType.isVoidType()) {
+      builder.addMoveResult(state.push(method.proto.returnType).register);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index ca6c1de..f93709e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -19,7 +19,12 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
+import java.util.ArrayList;
 import java.util.List;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.MethodVisitor;
@@ -83,4 +88,26 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerCallSite(callSite);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    DexType[] parameterTypes = callSite.methodProto.parameters.values;
+    List<Integer> registers = new ArrayList<>(parameterTypes.length);
+    for (int register : state.popReverse(parameterTypes.length)) {
+      registers.add(register);
+    }
+    List<ValueType> types = new ArrayList<>(parameterTypes.length);
+    for (DexType value : parameterTypes) {
+      types.add(ValueType.fromDexType(value));
+    }
+    builder.addInvokeCustom(callSite, types, registers);
+    if (!callSite.methodProto.returnType.isVoidType()) {
+      builder.addMoveResult(state.push(callSite.methodProto.returnType).register);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
index 401e4e8..da24ade 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
@@ -4,6 +4,9 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
@@ -28,4 +31,14 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitLabel(getLabel());
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    // Intentionally left empty.
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
index 58c59a5..1f4c3e0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -54,4 +58,16 @@
   public int getLocalIndex() {
     return var;
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot local = state.read(var);
+    Slot stack = state.push(local);
+    builder.addMove(local.type, stack.register, local.register);
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
index 2aab288..a8527f1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -91,4 +95,33 @@
   public void write(MethodVisitor visitor, NamingLens lens) {
     visitor.visitInsn(getAsmOpcode());
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int right = state.pop().register;
+    int left = state.pop().register;
+    int dest = state.push(ValueType.fromNumericType(type)).register;
+    switch (opcode) {
+      case Shl:
+        builder.addShl(type, dest, left, right);
+        break;
+      case Shr:
+        builder.addShr(type, dest, left, right);
+        break;
+      case Ushr:
+        builder.addUshr(type, dest, left, right);
+        break;
+      case And:
+        builder.addAnd(type, dest, left, right);
+        break;
+      case Or:
+        builder.addOr(type, dest, left, right);
+        break;
+      case Xor:
+        builder.addXor(type, dest, left, right);
+        break;
+      default:
+        throw new Unreachable("CfLogicalBinop has unknown opcode " + opcode);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
index b38ef12..3b19b03 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
@@ -5,6 +5,10 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.ir.code.Monitor.Type;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -30,4 +34,15 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot object = state.pop();
+    builder.addMonitor(type, object.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index a2f3da2..2dfc8fa 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -3,9 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.code;
 
+import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -41,4 +45,16 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerTypeReference(type);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code)
+      throws ApiLevelException {
+    int[] dimensions = state.popReverse(this.dimensions);
+    builder.addMultiNewArray(type, state.push(type).register, dimensions);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
index db0019a..daac41d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -60,4 +64,10 @@
         throw new Unreachable("Invalid opcode for CfNeg " + opcode);
     }
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int value = state.pop().register;
+    builder.addNeg(type, state.push(ValueType.fromNumericType(type)).register, value);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNew.java b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
index b7261b6..70e5add 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNew.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
@@ -6,6 +6,9 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -36,4 +39,14 @@
   public void registerUse(UseRegistry registry, DexType clazz) {
     registry.registerNewInstance(type);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addNewInstance(state.push(type).register, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
index 708a414..92feb63 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
@@ -7,6 +7,10 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.DescriptorUtils;
 import org.objectweb.asm.MethodVisitor;
@@ -76,4 +80,16 @@
       registry.registerTypeReference(type);
     }
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot size = state.pop();
+    Slot push = state.push(type);
+    builder.addNewArrayEmpty(push.register, size.register, type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNop.java b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
index 2cdd37c..b6308f4 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
@@ -4,6 +4,9 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -19,4 +22,14 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    // Intentionally left empty.
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
index b585cb6..e9cefb7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -127,4 +131,10 @@
         throw new Unreachable("Unexpected CfNumberConversion opcode " + opcode);
     }
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int source = state.pop().register;
+    builder.addConversion(to, from, state.push(ValueType.fromNumericType(to)).register, source);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
index 169d252..7508ea0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
@@ -5,6 +5,9 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 
@@ -31,4 +34,10 @@
   public Position getPosition() {
     return position;
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    state.setPosition(position);
+    builder.addDebugPosition(position);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
index 7993919..532800d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -48,4 +52,15 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean isReturn() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot pop = state.pop();
+    builder.addReturn(pop.type, pop.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
index 7d98073..cac89e0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
@@ -4,6 +4,9 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -19,4 +22,14 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean isReturn() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    builder.addReturn();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
index efda3f6..a30c6e6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
@@ -4,8 +4,13 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -78,4 +83,160 @@
   public Opcode getOpcode() {
     return opcode;
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    switch (opcode) {
+      case Pop:
+        {
+          Slot pop = state.pop();
+          assert !pop.type.isWide();
+          break;
+        }
+      case Pop2:
+        {
+          Slot value = state.pop();
+          if (!value.type.isWide()) {
+            state.pop();
+            throw new Unimplemented("Building IR for Pop2 of narrow value not supported");
+          }
+          break;
+        }
+      case Dup:
+        {
+          Slot dupValue = state.peek();
+          assert !dupValue.type.isWide();
+          builder.addMove(dupValue.type, state.push(dupValue).register, dupValue.register);
+          break;
+        }
+      case DupX1:
+        {
+          Slot value1 = state.pop();
+          Slot value2 = state.pop();
+          assert !value1.type.isWide();
+          assert !value2.type.isWide();
+          dupX1(builder, state, value1, value2);
+          break;
+        }
+      case DupX2:
+        {
+          Slot value1 = state.pop();
+          Slot value2 = state.pop();
+          assert !value1.type.isWide();
+          if (value2.type.isWide()) {
+            dupX1(builder, state, value1, value2);
+            throw new Unimplemented("Building IR for DupX2 of wide value not supported");
+          } else {
+            Slot value3 = state.pop();
+            assert !value3.type.isWide();
+            // Input stack: ..., A:value3, B:value2, C:value1
+            Slot outValue1 = state.push(value1);
+            Slot outValue3 = state.push(value3);
+            Slot outValue2 = state.push(value2);
+            Slot outValue1Copy = state.push(value1);
+            // Output stack: ..., A:outValue1, B:outValue3, C:outValue2, D:outValue1Copy
+            // Move D(outValue1Copy) <- C(value1)
+            builder.addMove(value1.type, outValue1Copy.register, value1.register);
+            // Move C(outValue2) <- B(value2)
+            builder.addMove(value2.type, outValue2.register, value2.register);
+            // Move B(outValue3) <- A(value3)
+            builder.addMove(value3.type, outValue3.register, value3.register);
+            // Move A(outValue1) <- D(outValue1Copy)
+            builder.addMove(value1.type, outValue1.register, outValue1Copy.register);
+          }
+          break;
+        }
+      case Dup2:
+        {
+          Slot value1 = state.peek();
+          if (value1.type.isWide()) {
+            builder.addMove(value1.type, state.push(value1).register, value1.register);
+          } else {
+            Slot value2 = state.peek(1);
+            builder.addMove(value2.type, state.push(value2).register, value2.register);
+            builder.addMove(value1.type, state.push(value1).register, value1.register);
+          }
+          break;
+        }
+      case Dup2X1:
+        {
+          Slot value1 = state.pop();
+          Slot value2 = state.pop();
+          assert !value2.type.isWide();
+          if (value1.type.isWide()) {
+            dupX1(builder, state, value1, value2);
+          } else {
+            // Input stack: ..., A:value3, B:value2, C:value1
+            Slot value3 = state.pop();
+            assert !value3.type.isWide();
+            Slot outValue2 = state.push(value2);
+            Slot outValue1 = state.push(value1);
+            Slot outValue3 = state.push(value3);
+            Slot outValue2Copy = state.push(value2);
+            Slot outValue1Copy = state.push(value1);
+            // Output: ..., A:outValue2, B:outValue1, C:outValue3, D:outValue2Copy, E:outValue1Copy
+            // Move E(outValue1Copy) <- C(value1)
+            builder.addMove(value1.type, outValue1Copy.register, value1.register);
+            // Move D(outValue2Copy) <- B(value2)
+            builder.addMove(value2.type, outValue2Copy.register, value2.register);
+            // Move C(outValue3) <- A(value3)
+            builder.addMove(value3.type, outValue3.register, value3.register);
+            // Move B(outValue1) <- E(outValue1Copy)
+            builder.addMove(value1.type, outValue1.register, outValue1Copy.register);
+            // Move A(outValue2) <- D(outValue2Copy)
+            builder.addMove(value2.type, outValue2.register, outValue2Copy.register);
+            throw new Unimplemented("Building IR for Dup2X1 narrow not supported");
+          }
+          break;
+        }
+      case Dup2X2:
+        {
+          // Input stack:
+          Slot value1 = state.pop();
+          Slot value2 = state.pop();
+          assert !value2.type.isWide();
+          if (value1.type.isWide()) {
+            // Input stack: ..., value2, value1
+            dupX1(builder, state, value1, value2);
+            // Output stack: ..., value1, value2, value1
+            throw new Unimplemented("Building IR for Dup2X2 wide not supported");
+          } else {
+            throw new Unimplemented("Building IR for Dup2X2 narrow not supported");
+          }
+          // break;
+        }
+      case Swap:
+        {
+          Slot value1 = state.pop();
+          Slot value2 = state.pop();
+          assert !value1.type.isWide();
+          assert !value2.type.isWide();
+          // Input stack: ..., value2, value1
+          dupX1(builder, state, value1, value2);
+          // Current stack: ..., value1, value2, value1copy
+          state.pop();
+          // Output stack: ..., value1, value2
+          break;
+        }
+    }
+  }
+
+  private void dupX1(IRBuilder builder, CfState state, Slot inValue1, Slot inValue2) {
+    // Input stack: ..., A:inValue2, B:inValue1 (values already popped)
+    Slot outValue1 = state.push(inValue1);
+    Slot outValue2 = state.push(inValue2);
+    Slot outValue1Copy = state.push(inValue1);
+    // Output stack: ..., A:outValue1, B:outValue2, C:outValue1Copy
+    // Move C(outValue1Copy) <- B(inValue1)
+    builder.addMove(inValue1.type, outValue1Copy.register, inValue1.register);
+    // Move B(outValue2) <- A(inValue2)
+    builder.addMove(inValue2.type, outValue2.register, inValue2.register);
+    // Move A(outValue1) <- C(outValue1Copy)
+    builder.addMove(outValue1Copy.type, outValue1.register, outValue1Copy.register);
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStore.java b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
index 817045c..f364eaa 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
@@ -6,6 +6,10 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -54,4 +58,15 @@
   public int getLocalIndex() {
     return var;
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot pop = state.pop();
+    builder.addMove(type, state.write(var, pop).register, pop.register);
+  }
+
+  @Override
+  public boolean emitsIR() {
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
index 1dc72d4..e0ed773 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
@@ -41,7 +45,7 @@
     return new IntArrayList(keys);
   }
 
-  public List<CfLabel> getTargets() {
+  public List<CfLabel> getSwitchTargets() {
     return targets;
   }
 
@@ -67,4 +71,14 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    int[] labelOffsets = new int[targets.size()];
+    for (int i = 0; i < targets.size(); i++) {
+      labelOffsets[i] = code.getLabelOffset(targets.get(i));
+    }
+    Slot value = state.pop();
+    builder.addSwitch(value.register, keys, code.getLabelOffset(defaultTarget), labelOffsets);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
index ef73d10..483d3a8 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
@@ -4,6 +4,10 @@
 package com.android.tools.r8.cf.code;
 
 import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.CfState;
+import com.android.tools.r8.ir.conversion.CfState.Slot;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.NamingLens;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -19,4 +23,15 @@
   public void print(CfPrinter printer) {
     printer.print(this);
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
+
+  @Override
+  public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    Slot exception = state.pop();
+    builder.addThrow(exception.register);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
index 6dceda0..5c1aa30 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
@@ -21,6 +21,14 @@
     this.end = end;
     this.guards = guards;
     this.targets = targets;
+    assert verifyAllNonNull(guards);
+  }
+
+  private static boolean verifyAllNonNull(List<DexType> types) {
+    for (DexType type : types) {
+      assert type != null;
+    }
+    return true;
   }
 
   public static CfTryCatch fromBuilder(
diff --git a/src/main/java/com/android/tools/r8/code/ConstMethodType.java b/src/main/java/com/android/tools/r8/code/ConstMethodType.java
index aba9e08..6a1bef5 100644
--- a/src/main/java/com/android/tools/r8/code/ConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/code/ConstMethodType.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.graph.OffsetToObjectMapping;
+import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.ClassNameMapper;
 import java.nio.ShortBuffer;
@@ -56,6 +57,11 @@
   }
 
   @Override
+  public void registerUse(UseRegistry registry) {
+    registry.registerProto(getMethodType());
+  }
+
+  @Override
   public void write(ShortBuffer dest, ObjectToOffsetMapping mapping) {
     int index = BBBB.getOffset(mapping);
     if (index != (index & 0xffff)) {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index df16622..382bc66 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexDebugInfo;
@@ -30,6 +29,7 @@
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -106,7 +106,7 @@
     }
 
     @Override
-    public boolean add(DexAnnotationSetRefList annotationSetRefList) {
+    public boolean add(ParameterAnnotationsList parameterAnnotationsList) {
       return true;
     }
 
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 8042a1a..d762646 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexCode;
@@ -56,6 +55,7 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.OffsetToObjectMapping;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
@@ -340,11 +340,11 @@
     return result;
   }
 
-  private DexAnnotationSetRefList annotationSetRefListAt(int offset) {
-    return (DexAnnotationSetRefList) cacheAt(offset, this::parseAnnotationSetRefList);
+  private ParameterAnnotationsList annotationSetRefListAt(int offset) {
+    return (ParameterAnnotationsList) cacheAt(offset, this::parseAnnotationSetRefList);
   }
 
-  private DexAnnotationSetRefList parseAnnotationSetRefList() {
+  private ParameterAnnotationsList parseAnnotationSetRefList() {
     int size = dexReader.getUint();
     int[] annotationOffsets = new int[size];
     for (int i = 0; i < size; i++) {
@@ -354,7 +354,7 @@
     for (int i = 0; i < size; i++) {
       values[i] = annotationSetAt(annotationOffsets[i]);
     }
-    return new DexAnnotationSetRefList(values);
+    return new ParameterAnnotationsList(values);
   }
 
   private DexParameterAnnotation[] parseParameterAnnotations(int size) {
@@ -373,7 +373,8 @@
       DexMethod method = indexedItems.getMethod(methodIndices[i]);
       result[i] = new DexParameterAnnotation(
           method,
-          annotationSetRefListAt(annotationOffsets[i]));
+          annotationSetRefListAt(annotationOffsets[i])
+              .withParameterCount(method.proto.parameters.size()));
     }
     dexReader.position(saved);
     return result;
@@ -589,8 +590,8 @@
     int methodIndex = 0;
     MemberAnnotationIterator<DexMethod, DexAnnotationSet> annotationIterator =
         new MemberAnnotationIterator<>(annotations, DexAnnotationSet::empty);
-    MemberAnnotationIterator<DexMethod, DexAnnotationSetRefList> parameterAnnotationsIterator =
-        new MemberAnnotationIterator<>(parameters, DexAnnotationSetRefList::empty);
+    MemberAnnotationIterator<DexMethod, ParameterAnnotationsList> parameterAnnotationsIterator =
+        new MemberAnnotationIterator<>(parameters, ParameterAnnotationsList::empty);
     for (int i = 0; i < size; i++) {
       methodIndex += dexReader.getUleb128();
       MethodAccessFlags accessFlags = MethodAccessFlags.fromDexAccessFlags(dexReader.getUleb128());
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index ced33f6..4767c64 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.graph.DexAnnotationDirectory;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
@@ -40,6 +39,7 @@
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.graph.KeyedDexItem;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.graph.ProgramClassVisitor;
 import com.android.tools.r8.logging.Log;
@@ -510,12 +510,17 @@
     }
   }
 
-  private void writeAnnotationSetRefList(DexAnnotationSetRefList setRefList) {
-    assert !setRefList.isEmpty();
-    mixedSectionOffsets.setOffsetFor(setRefList, dest.align(4));
-    dest.putInt(setRefList.values.length);
-    for (DexAnnotationSet set : setRefList.values) {
-      dest.putInt(mixedSectionOffsets.getOffsetFor(set));
+  private void writeAnnotationSetRefList(ParameterAnnotationsList parameterAnnotationsList) {
+    assert !parameterAnnotationsList.isEmpty();
+    mixedSectionOffsets.setOffsetFor(parameterAnnotationsList, dest.align(4));
+    dest.putInt(parameterAnnotationsList.countNonMissing());
+    for (int i = 0; i < parameterAnnotationsList.size(); i++) {
+      if (parameterAnnotationsList.isMissing(i)) {
+        // b/62300145: Maintain broken ParameterAnnotations attribute by only outputting the
+        // non-missing annotation lists.
+        continue;
+      }
+      dest.putInt(mixedSectionOffsets.getOffsetFor(parameterAnnotationsList.get(i)));
     }
   }
 
@@ -541,7 +546,7 @@
     writeMemberAnnotations(methodAnnotations,
         item -> mixedSectionOffsets.getOffsetFor(item.annotations));
     writeMemberAnnotations(parameterAnnotations,
-        item -> mixedSectionOffsets.getOffsetFor(item.parameterAnnotations));
+        item -> mixedSectionOffsets.getOffsetFor(item.parameterAnnotationsList));
   }
 
   private void writeEncodedFields(DexEncodedField[] fields) {
@@ -990,7 +995,7 @@
     private final Reference2IntMap<DexString> stringData = createReference2IntMap();
     private final Object2IntMap<DexAnnotation> annotations = createObject2IntMap();
     private final Object2IntMap<DexAnnotationSet> annotationSets = createObject2IntMap();
-    private final Object2IntMap<DexAnnotationSetRefList> annotationSetRefLists
+    private final Object2IntMap<ParameterAnnotationsList> annotationSetRefLists
         = createObject2IntMap();
     private final Object2IntMap<DexAnnotationDirectory> annotationDirectories
         = createObject2IntMap();
@@ -1072,7 +1077,7 @@
     }
 
     @Override
-    public boolean add(DexAnnotationSetRefList annotationSetRefList) {
+    public boolean add(ParameterAnnotationsList annotationSetRefList) {
       if (annotationSetRefList.isEmpty()) {
         return false;
       }
@@ -1120,7 +1125,7 @@
       return annotationSets.keySet();
     }
 
-    public Collection<DexAnnotationSetRefList> getAnnotationSetRefLists() {
+    public Collection<ParameterAnnotationsList> getAnnotationSetRefLists() {
       return annotationSetRefLists.keySet();
     }
 
@@ -1200,7 +1205,7 @@
       return lookup(annotationSet, annotationSets);
     }
 
-    public int getOffsetFor(DexAnnotationSetRefList annotationSetRefList) {
+    public int getOffsetFor(ParameterAnnotationsList annotationSetRefList) {
       if (annotationSetRefList.isEmpty()) {
         return 0;
       }
@@ -1261,7 +1266,7 @@
       setOffsetFor(encodedArray, offset, encodedArrays);
     }
 
-    void setOffsetFor(DexAnnotationSetRefList annotationSetRefList, int offset) {
+    void setOffsetFor(ParameterAnnotationsList annotationSetRefList, int offset) {
       assert offset != 0 && !annotationSetRefList.isEmpty();
       setOffsetFor(annotationSetRefList, offset, annotationSetRefLists);
     }
diff --git a/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java b/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java
index eb7ae98..dee12fc 100644
--- a/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java
+++ b/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java
@@ -6,13 +6,13 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationDirectory;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexDebugInfo;
 import com.android.tools.r8.graph.DexEncodedArray;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 
 /**
  * Collection of the various components of the mixed section of a dex file.
@@ -86,7 +86,7 @@
    *
    * @return true if the item was not added before
    */
-  public abstract boolean add(DexAnnotationSetRefList annotationSetRefList);
+  public abstract boolean add(ParameterAnnotationsList annotationSetRefList);
 
   /**
    * Adds the given annotation to the collection.
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 2ba0890..7daef42 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,14 +3,21 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 
@@ -62,6 +69,11 @@
     return app.definitionFor(type);
   }
 
+  public Origin originFor(DexType type) {
+    DexClass definition = app.definitionFor(type);
+    return definition == null ? Origin.unknown() : definition.origin;
+  }
+
   public DexEncodedMethod definitionFor(DexMethod method) {
     return (DexEncodedMethod) getDefinitions(method.getHolder()).get(method);
   }
@@ -469,6 +481,40 @@
     return result;
   }
 
+  public boolean canTriggerStaticInitializer(DexType clazz) {
+    Set<DexType> knownInterfaces = Sets.newIdentityHashSet();
+
+    // Process superclass chain.
+    while (clazz != null && clazz != dexItemFactory.objectType) {
+      DexClass definition = definitionFor(clazz);
+      if (definition == null || definition.hasClassInitializer()) {
+        return true; // Assume it *may* trigger if we didn't find the definition.
+      }
+      knownInterfaces.addAll(Arrays.asList(definition.interfaces.values));
+      clazz = definition.superType;
+    }
+
+    // Process interfaces.
+    Queue<DexType> queue = new ArrayDeque<>(knownInterfaces);
+    while (!queue.isEmpty()) {
+      DexType iface = queue.remove();
+      DexClass definition = definitionFor(iface);
+      if (definition == null || definition.hasClassInitializer()) {
+        return true; // Assume it *may* trigger if we didn't find the definition.
+      }
+      if (!definition.accessFlags.isInterface()) {
+        throw new Unreachable(iface.toSourceString() + " is expected to be an interface");
+      }
+
+      for (DexType superIface : definition.interfaces.values) {
+        if (knownInterfaces.add(superIface)) {
+          queue.add(superIface);
+        }
+      }
+    }
+    return false;
+  }
+
   public interface ResolutionResult {
 
     DexEncodedMethod asResultOfResolve();
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index ab1eca5..6e93a90 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -5,28 +5,22 @@
 
 import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.cf.CfPrinter;
-import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfPosition;
 import com.android.tools.r8.cf.code.CfReturnVoid;
-import com.android.tools.r8.cf.code.CfThrow;
 import com.android.tools.r8.cf.code.CfTryCatch;
 import com.android.tools.r8.errors.Unimplemented;
-import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.Position;
-import com.android.tools.r8.ir.code.Throw;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
-import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfSourceCode;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
@@ -120,6 +114,29 @@
   }
 
   @Override
+  public int estimatedSizeForInlining() {
+    return countNonStackOperations(Integer.MAX_VALUE);
+  }
+
+  @Override
+  public boolean estimatedSizeForInliningAtMost(int threshold) {
+    return countNonStackOperations(threshold) <= threshold;
+  }
+
+  private int countNonStackOperations(int threshold) {
+    int result = 0;
+    for (CfInstruction instruction : instructions) {
+      if (instruction.emitsIR()) {
+        result++;
+        if (result > threshold) {
+          break;
+        }
+      }
+    }
+    return result;
+  }
+
+  @Override
   public boolean isCfCode() {
     return true;
   }
@@ -183,34 +200,46 @@
   }
 
   @Override
-  public IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public IRCode buildIR(
+      DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
+      InternalOptions options,
+      Origin origin)
       throws ApiLevelException {
-    if (instructions.size() == 2
-        && instructions.get(0) instanceof CfConstNull
-        && instructions.get(1) instanceof CfThrow) {
-      BasicBlock block = new BasicBlock();
-      block.setNumber(1);
-      Value nullValue = new Value(0, ValueType.OBJECT, null);
-      block.add(new ConstNumber(nullValue, 0L));
-      block.add(new Throw(nullValue));
-      block.close(null);
-      for (Instruction insn : block.getInstructions()) {
-        insn.setPosition(Position.none());
-      }
-      LinkedList<BasicBlock> blocks = new LinkedList<>(Collections.singleton(block));
-      return new IRCode(options, encodedMethod, blocks, null, false);
-    }
-    throw new Unimplemented("Converting Java class-file bytecode to IR not yet supported");
+    return internalBuild(encodedMethod, appInfo, options, null, null, origin);
   }
 
   @Override
   public IRCode buildInliningIR(
       DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
       InternalOptions options,
       ValueNumberGenerator valueNumberGenerator,
-      Position callerPosition)
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
-    throw new Unimplemented("Converting Java class-file bytecode to IR not yet supported");
+    assert valueNumberGenerator != null;
+    assert callerPosition != null;
+    return internalBuild(
+        encodedMethod, appInfo, options, valueNumberGenerator, callerPosition, origin);
+  }
+
+  private IRCode internalBuild(
+      DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
+      InternalOptions options,
+      ValueNumberGenerator generator,
+      Position callerPosition,
+      Origin origin)
+      throws ApiLevelException {
+    assert !options.isGeneratingDex() || !encodedMethod.accessFlags.isSynchronized()
+        : "Converting CfCode to IR not supported for DEX output of synchronized methods.";
+    CfSourceCode source = new CfSourceCode(this, encodedMethod, callerPosition, origin);
+    IRBuilder builder =
+        (generator == null)
+            ? new IRBuilder(encodedMethod, appInfo, source, options)
+            : new IRBuilder(encodedMethod, appInfo, source, options, generator);
+    return builder.build();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index db9306d..6f86412 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -12,18 +12,25 @@
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.optimize.Outliner.OutlineCode;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 
 public abstract class Code extends CachedHashValueDexItem {
 
-  public abstract IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public abstract IRCode buildIR(
+      DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
+      InternalOptions options,
+      Origin origin)
       throws ApiLevelException;
 
   public IRCode buildInliningIR(
       DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
       InternalOptions options,
       ValueNumberGenerator valueNumberGenerator,
-      Position callerPosition)
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
     throw new Unreachable("Unexpected attempt to build IR graph for inlining from: "
         + getClass().getCanonicalName());
@@ -52,10 +59,16 @@
     return false;
   }
 
+  /** Estimate the number of IR instructions emitted by buildIR(). */
   public int estimatedSizeForInlining() {
     return Integer.MAX_VALUE;
   }
 
+  /** Compute estimatedSizeForInlining() <= threshold. */
+  public boolean estimatedSizeForInliningAtMost(int threshold) {
+    return estimatedSizeForInlining() <= threshold;
+  }
+
   public CfCode asCfCode() {
     throw new Unreachable(getClass().getCanonicalName() + ".asCfCode()");
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
index aa5d145..fa5d8a8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
@@ -33,7 +33,7 @@
       if (!method.annotations.isEmpty()) {
         methodAnnotations.add(method);
       }
-      if (!method.parameterAnnotations.isEmpty()) {
+      if (!method.parameterAnnotationsList.isEmpty()) {
         parameterAnnotations.add(method);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
index 9ee8fba..d093a20 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
@@ -5,7 +5,9 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.function.Predicate;
 
 public class DexAnnotationSet extends CachedHashValueDexItem {
 
@@ -115,4 +117,30 @@
     extendedArray[annotations.length] = newAnnotation;
     return new DexAnnotationSet(extendedArray);
   }
+
+  public DexAnnotationSet keepIf(Predicate<DexAnnotation> filter) {
+    ArrayList<DexAnnotation> filtered = null;
+    for (int i = 0; i < annotations.length; i++) {
+      DexAnnotation annotation = annotations[i];
+      if (filter.test(annotation)) {
+        if (filtered != null) {
+          filtered.add(annotation);
+        }
+      } else {
+        if (filtered == null) {
+          filtered = new ArrayList<>(annotations.length);
+          for (int j = 0; j < i; j++) {
+            filtered.add(annotations[j]);
+          }
+        }
+      }
+    }
+    if (filtered == null) {
+      return this;
+    } else if (filtered.isEmpty()) {
+      return DexAnnotationSet.empty();
+    } else {
+      return new DexAnnotationSet(filtered.toArray(new DexAnnotation[filtered.size()]));
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java
deleted file mode 100644
index 4bb1314..0000000
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// 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.
-package com.android.tools.r8.graph;
-
-import com.android.tools.r8.dex.IndexedItemCollection;
-import com.android.tools.r8.dex.MixedSectionCollection;
-import java.util.Arrays;
-
-public class DexAnnotationSetRefList extends DexItem {
-
-  private static final DexAnnotationSetRefList theEmptyTypeList = new DexAnnotationSetRefList();
-
-  public final DexAnnotationSet[] values;
-  private final int missingParameterAnnotations;
-
-  public static DexAnnotationSetRefList empty() {
-    return theEmptyTypeList;
-  }
-
-  private DexAnnotationSetRefList() {
-    this.values = new DexAnnotationSet[0];
-    this.missingParameterAnnotations = 0;
-  }
-
-  public DexAnnotationSetRefList(DexAnnotationSet[] values) {
-    this(values, 0);
-  }
-
-  public DexAnnotationSetRefList(DexAnnotationSet[] values, int missingParameterAnnotations) {
-    assert values != null && values.length > 0;
-    this.values = values;
-    this.missingParameterAnnotations = missingParameterAnnotations;
-  }
-
-  @Override
-  public int hashCode() {
-    return Arrays.hashCode(values);
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    if (this == other) {
-      return true;
-    }
-    if (other instanceof DexAnnotationSetRefList) {
-      return Arrays.equals(values, ((DexAnnotationSetRefList) other).values);
-    }
-    return false;
-  }
-
-  @Override
-  public void collectIndexedItems(IndexedItemCollection indexedItems,
-      DexMethod method, int instructionOffset) {
-    collectAll(indexedItems, values);
-  }
-
-  @Override
-  void collectMixedSectionItems(MixedSectionCollection mixedItems) {
-    // Collect values first so that the annotation sets have sorted themselves before adding this.
-    collectAll(mixedItems, values);
-    mixedItems.add(this);
-  }
-
-  public boolean isEmpty() {
-    return values.length == 0;
-  }
-
-  public int getMissingParameterAnnotations() {
-    return missingParameterAnnotations;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 7b09cab..23139c5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -159,21 +159,13 @@
       for (DexAnnotation annotation : method.annotations.annotations) {
         consumer.accept(annotation);
       }
-      for (DexAnnotationSet parameterAnnotations : method.parameterAnnotations.values) {
-        for (DexAnnotation annotation : parameterAnnotations.annotations) {
-          consumer.accept(annotation);
-        }
-      }
+      method.parameterAnnotationsList.forEachAnnotation(consumer);
     }
     for (DexEncodedMethod method : virtualMethods()) {
       for (DexAnnotation annotation : method.annotations.annotations) {
         consumer.accept(annotation);
       }
-      for (DexAnnotationSet parameterAnnotations : method.parameterAnnotations.values) {
-        for (DexAnnotation annotation : parameterAnnotations.annotations) {
-          consumer.accept(annotation);
-        }
-      }
+      method.parameterAnnotationsList.forEachAnnotation(consumer);
     }
     for (DexEncodedField field : instanceFields()) {
       for (DexAnnotation annotation : field.annotations.annotations) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index a023357..685f32e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.conversion.DexSourceCode;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.StringUtils;
@@ -163,21 +164,23 @@
   }
 
   @Override
-  public IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public IRCode buildIR(DexEncodedMethod encodedMethod, AppInfo appInfo,
+      InternalOptions options, Origin origin)
       throws ApiLevelException {
     DexSourceCode source =
         new DexSourceCode(
             this, encodedMethod, null, options.lineNumberOptimization == LineNumberOptimization.ON);
-    IRBuilder builder = new IRBuilder(encodedMethod, source, options);
+    IRBuilder builder = new IRBuilder(encodedMethod, appInfo, source, options);
     return builder.build();
   }
 
   @Override
   public IRCode buildInliningIR(
       DexEncodedMethod encodedMethod,
-      InternalOptions options,
+      AppInfo appInfo, InternalOptions options,
       ValueNumberGenerator valueNumberGenerator,
-      Position callerPosition)
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
     DexSourceCode source =
         new DexSourceCode(
@@ -185,7 +188,8 @@
             encodedMethod,
             callerPosition,
             options.lineNumberOptimization == LineNumberOptimization.ON);
-    IRBuilder builder = new IRBuilder(encodedMethod, source, options, valueNumberGenerator);
+    IRBuilder builder =
+        new IRBuilder(encodedMethod, appInfo, source, options, valueNumberGenerator);
     return builder.build();
   }
 
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 d6bde0b..ee29ff3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -42,10 +42,14 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableMap;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
 
 public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
@@ -92,7 +96,7 @@
   public final DexMethod method;
   public final MethodAccessFlags accessFlags;
   public DexAnnotationSet annotations;
-  public DexAnnotationSetRefList parameterAnnotations;
+  public ParameterAnnotationsList parameterAnnotationsList;
   private Code code;
   private CompilationState compilationState = CompilationState.NOT_PROCESSED;
   private OptimizationInfo optimizationInfo = DefaultOptimizationInfo.DEFAULT;
@@ -101,12 +105,12 @@
       DexMethod method,
       MethodAccessFlags accessFlags,
       DexAnnotationSet annotations,
-      DexAnnotationSetRefList parameterAnnotations,
+      ParameterAnnotationsList parameterAnnotationsList,
       Code code) {
     this.method = method;
     this.accessFlags = accessFlags;
     this.annotations = annotations;
-    this.parameterAnnotations = parameterAnnotations;
+    this.parameterAnnotationsList = parameterAnnotationsList;
     this.code = code;
     assert code == null || !accessFlags.isAbstract();
   }
@@ -227,20 +231,25 @@
     compilationState = CompilationState.NOT_PROCESSED;
   }
 
-  public IRCode buildIR(InternalOptions options) throws ApiLevelException {
-    return code == null ? null : code.buildIR(this, options);
+  public IRCode buildIR(
+      AppInfo appInfo, InternalOptions options, Origin origin) throws ApiLevelException {
+    return code == null ? null : code.buildIR(this, appInfo, options, origin);
   }
 
   public IRCode buildInliningIRForTesting(
       InternalOptions options, ValueNumberGenerator valueNumberGenerator)
       throws ApiLevelException {
-    return buildInliningIR(options, valueNumberGenerator, null);
+    return buildInliningIR(null, options, valueNumberGenerator, null, Origin.unknown());
   }
 
   public IRCode buildInliningIR(
-      InternalOptions options, ValueNumberGenerator valueNumberGenerator, Position callerPosition)
+      AppInfo appInfo, InternalOptions options,
+      ValueNumberGenerator valueNumberGenerator,
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
-    return code.buildInliningIR(this, options, valueNumberGenerator, callerPosition);
+    return code.buildInliningIR(
+        this, appInfo, options, valueNumberGenerator, callerPosition, origin);
   }
 
   public void setCode(Code code) {
@@ -268,7 +277,7 @@
       code.collectIndexedItems(indexedItems, this.method);
     }
     annotations.collectIndexedItems(indexedItems);
-    parameterAnnotations.collectIndexedItems(indexedItems);
+    parameterAnnotationsList.collectIndexedItems(indexedItems);
   }
 
   @Override
@@ -277,7 +286,7 @@
       code.collectMixedSectionItems(mixedItems);
     }
     annotations.collectMixedSectionItems(mixedItems);
-    parameterAnnotations.collectMixedSectionItems(mixedItems);
+    parameterAnnotationsList.collectMixedSectionItems(mixedItems);
   }
 
   public Code getCode() {
@@ -533,7 +542,7 @@
   }
 
   public boolean hasAnnotation() {
-    return !annotations.isEmpty() || !parameterAnnotations.isEmpty();
+    return !annotations.isEmpty() || !parameterAnnotationsList.isEmpty();
   }
 
   public void registerCodeReferences(UseRegistry registry) {
@@ -560,6 +569,8 @@
     private boolean useIdentifierNameString = false;
     private boolean checksNullReceiverBeforeAnySideEffect = false;
     private boolean triggersClassInitBeforeAnySideEffect = false;
+    private Set<DexField> receiverOnlyUsedForReadingFields = null;
+    private Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects = null;
 
     private OptimizationInfo() {
       // Intentionally left empty.
@@ -596,6 +607,15 @@
       return returnsConstant;
     }
 
+    public boolean isReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+      return receiverOnlyUsedForReadingFields != null &&
+          fields.containsAll(receiverOnlyUsedForReadingFields);
+    }
+
+    public Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects() {
+      return onlyInitializesFieldsWithNoOtherSideEffects;
+    }
+
     public long getReturnedConstant() {
       assert returnsConstant();
       return returnedConstant;
@@ -637,6 +657,19 @@
       returnedConstant = value;
     }
 
+    private void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+      receiverOnlyUsedForReadingFields = fields;
+    }
+
+    private void markOnlyInitializesFieldsWithNoOtherSideEffects(Map<DexField, Integer> mapping) {
+      if (mapping == null) {
+        onlyInitializesFieldsWithNoOtherSideEffects = null;
+      } else {
+        onlyInitializesFieldsWithNoOtherSideEffects = mapping.isEmpty()
+            ? Collections.emptyMap() : ImmutableMap.copyOf(mapping);
+      }
+    }
+
     private void markForceInline() {
       forceInline = true;
     }
@@ -694,6 +727,15 @@
     ensureMutableOI().markReturnsConstant(value);
   }
 
+  synchronized public void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
+    ensureMutableOI().markReceiverOnlyUsedForReadingFields(fields);
+  }
+
+  synchronized public void markOnlyInitializesFieldsWithNoOtherSideEffects(
+      Map<DexField, Integer> mapping) {
+    ensureMutableOI().markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  }
+
   synchronized public void markForceInline() {
     ensureMutableOI().markForceInline();
   }
@@ -723,7 +765,7 @@
     private DexMethod method;
     private final MethodAccessFlags accessFlags;
     private final DexAnnotationSet annotations;
-    private final DexAnnotationSetRefList parameterAnnotations;
+    private final ParameterAnnotationsList parameterAnnotations;
     private Code code;
     private CompilationState compilationState = CompilationState.NOT_PROCESSED;
     private OptimizationInfo optimizationInfo = DefaultOptimizationInfo.DEFAULT;
@@ -733,7 +775,7 @@
       method = from.method;
       accessFlags = from.accessFlags.copy();
       annotations = from.annotations;
-      parameterAnnotations = from.parameterAnnotations;
+      parameterAnnotations = from.parameterAnnotationsList;
       code = from.code;
       compilationState = from.compilationState;
       optimizationInfo = from.optimizationInfo.copy();
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 9e77dd5..86ca12a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -115,6 +115,7 @@
   public final DexString valueOfMethodName = createString("valueOf");
 
   public final DexString getClassMethodName = createString("getClass");
+  public final DexString finalizeMethodName = createString("finalize");
   public final DexString ordinalMethodName = createString("ordinal");
   public final DexString desiredAssertionStatusMethodName = createString("desiredAssertionStatus");
   public final DexString forNameMethodName = createString("forName");
@@ -125,6 +126,8 @@
   public final DexString getMethodName = createString("getMethod");
   public final DexString getDeclaredMethodName = createString("getDeclaredMethod");
   public final DexString assertionsDisabled = createString("$assertionsDisabled");
+  public final DexString invokeMethodName = createString("invoke");
+  public final DexString invokeExactMethodName = createString("invokeExact");
 
   public final DexString stringDescriptor = createString("Ljava/lang/String;");
   public final DexString stringArrayDescriptor = createString("[Ljava/lang/String;");
@@ -209,6 +212,7 @@
   public final AtomicFieldUpdaterMethods atomicFieldUpdaterMethods =
       new AtomicFieldUpdaterMethods();
   public final Kotlin kotlin;
+  public final PolymorphicMethods polymorphicMethods = new PolymorphicMethods();
 
   // Dex system annotations.
   // See https://source.android.com/devices/tech/dalvik/dex-format.html#system-annotation
@@ -234,6 +238,7 @@
   public final DexType callSiteType = createType("Ljava/lang/invoke/CallSite;");
   public final DexType lookupType = createType("Ljava/lang/invoke/MethodHandles$Lookup;");
   public final DexType serializableType = createType("Ljava/io/Serializable;");
+  public final DexType comparableType = createType("Ljava/lang/Comparable;");
 
   public final DexMethod metafactoryMethod =
       createMethod(
@@ -322,12 +327,15 @@
 
     public final DexMethod getClass;
     public final DexMethod constructor;
+    public final DexMethod finalize;
 
     private ObjectMethods() {
       getClass = createMethod(objectDescriptor, getClassMethodName, classDescriptor,
           DexString.EMPTY_ARRAY);
       constructor = createMethod(objectDescriptor,
           constructorMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
+      finalize = createMethod(objectDescriptor,
+          finalizeMethodName, voidType.descriptor, DexString.EMPTY_ARRAY);
     }
   }
 
@@ -471,6 +479,75 @@
     }
   }
 
+  public class PolymorphicMethods {
+
+    private final DexProto signature = createProto(objectType, objectArrayType);
+    private final DexProto setSignature = createProto(voidType, objectArrayType);
+    private final DexProto compareAndSetSignature = createProto(booleanType, objectArrayType);
+
+    private final Set<DexString> varHandleMethods =
+        createStrings(
+            "compareAndExchange",
+            "compareAndExchangeAcquire",
+            "compareAndExchangeRelease",
+            "get",
+            "getAcquire",
+            "getAndAdd",
+            "getAndAddAcquire",
+            "getAndAddRelease",
+            "getAndBitwiseAnd",
+            "getAndBitwiseAndAcquire",
+            "getAndBitwiseAndRelease",
+            "getAndBitwiseOr",
+            "getAndBitwiseOrAcquire",
+            "getAndBitwiseOrRelease",
+            "getAndBitwiseXor",
+            "getAndBitwiseXorAcquire",
+            "getAndBitwiseXorRelease",
+            "getAndSet",
+            "getAndSetAcquire",
+            "getAndSetRelease",
+            "getOpaque",
+            "getVolatile");
+
+    private final Set<DexString> varHandleSetMethods =
+        createStrings("set", "setOpaque", "setRelease", "setVolatile");
+
+    private final Set<DexString> varHandleCompareAndSetMethods =
+        createStrings(
+            "compareAndSet",
+            "weakCompareAndSet",
+            "weakCompareAndSetAcquire",
+            "weakCompareAndSetPlain",
+            "weakCompareAndSetRelease");
+
+    public DexMethod canonicalize(DexMethod invokeProto) {
+      if (invokeProto.holder == methodHandleType) {
+        if (invokeProto.name == invokeMethodName || invokeProto.name == invokeExactMethodName) {
+          return createMethod(methodHandleType, signature, invokeProto.name);
+        }
+      } else if (invokeProto.holder == varHandleType) {
+        if (varHandleMethods.contains(invokeProto.name)) {
+          return createMethod(varHandleType, signature, invokeProto.name);
+        } else if (varHandleSetMethods.contains(invokeProto.name)) {
+          return createMethod(varHandleType, setSignature, invokeProto.name);
+        } else if (varHandleCompareAndSetMethods.contains(invokeProto.name)) {
+          return createMethod(varHandleType, compareAndSetSignature, invokeProto.name);
+        }
+      }
+      return null;
+    }
+
+    private Set<DexString> createStrings(String... strings) {
+      IdentityHashMap<DexString, DexString> map = new IdentityHashMap<>();
+      for (String string : strings) {
+        DexString dexString = createString(string);
+        map.put(dexString, dexString);
+      }
+      return map.keySet();
+    }
+  }
+
   private static <T extends DexItem> T canonicalize(ConcurrentHashMap<T, T> map, T item) {
     assert item != null;
     assert !DexItemFactory.isInternalSentinel(item);
diff --git a/src/main/java/com/android/tools/r8/graph/DexMemberAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexMemberAnnotation.java
index f005588..6cc2fb4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMemberAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMemberAnnotation.java
@@ -60,9 +60,9 @@
   }
 
   public static class DexParameterAnnotation extends
-      DexMemberAnnotation<DexMethod, DexAnnotationSetRefList> {
+      DexMemberAnnotation<DexMethod, ParameterAnnotationsList> {
 
-    public DexParameterAnnotation(DexMethod item, DexAnnotationSetRefList annotations) {
+    public DexParameterAnnotation(DexMethod item, ParameterAnnotationsList annotations) {
       super(item, annotations);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index e403c14..02f0801 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -446,7 +446,7 @@
     private List<DexAnnotation> annotations = null;
     private DexValue defaultAnnotation = null;
     private int fakeParameterAnnotations = 0;
-    private List<List<DexAnnotation>> parameterAnnotations = null;
+    private List<List<DexAnnotation>> parameterAnnotationsLists = null;
     private List<DexValue> parameterNames = null;
     private List<DexValue> parameterFlags = null;
     final DexMethod method;
@@ -508,21 +508,21 @@
         // We can iterate through all the parameters twice. Once for visible and once for
         // invisible parameter annotations. We only record the number of fake parameter
         // annotations once.
-        if (parameterAnnotations == null) {
+        if (parameterAnnotationsLists == null) {
           fakeParameterAnnotations++;
         }
         return null;
       }
-      if (parameterAnnotations == null) {
+      if (parameterAnnotationsLists == null) {
         int adjustedParameterCount = parameterCount - fakeParameterAnnotations;
-        parameterAnnotations = new ArrayList<>(adjustedParameterCount);
+        parameterAnnotationsLists = new ArrayList<>(adjustedParameterCount);
         for (int i = 0; i < adjustedParameterCount; i++) {
-          parameterAnnotations.add(new ArrayList<>());
+          parameterAnnotationsLists.add(new ArrayList<>());
         }
       }
       assert mv == null;
       return createAnnotationVisitor(desc, visible,
-          parameterAnnotations.get(parameter - fakeParameterAnnotations), parent.application);
+          parameterAnnotationsLists.get(parameter - fakeParameterAnnotations), parent.application);
     }
 
     @Override
@@ -573,15 +573,15 @@
     public void visitEnd() {
       assert flags.isAbstract() || flags.isNative() || parent.classKind != ClassKind.PROGRAM
           || code != null;
-      DexAnnotationSetRefList parameterAnnotationSets;
-      if (parameterAnnotations == null) {
-        parameterAnnotationSets = DexAnnotationSetRefList.empty();
+      ParameterAnnotationsList annotationsList;
+      if (parameterAnnotationsLists == null) {
+        annotationsList = ParameterAnnotationsList.empty();
       } else {
-        DexAnnotationSet[] sets = new DexAnnotationSet[parameterAnnotations.size()];
-        for (int i = 0; i < parameterAnnotations.size(); i++) {
-          sets[i] = createAnnotationSet(parameterAnnotations.get(i));
+        DexAnnotationSet[] sets = new DexAnnotationSet[parameterAnnotationsLists.size()];
+        for (int i = 0; i < parameterAnnotationsLists.size(); i++) {
+          sets[i] = createAnnotationSet(parameterAnnotationsLists.get(i));
         }
-        parameterAnnotationSets = new DexAnnotationSetRefList(sets, fakeParameterAnnotations);
+        annotationsList = new ParameterAnnotationsList(sets, fakeParameterAnnotations);
       }
       InternalOptions internalOptions = parent.application.options;
       if (parameterNames != null && internalOptions.canUseParameterNameAnnotations()) {
@@ -596,7 +596,7 @@
             parent.application.getFactory()));
       }
       DexEncodedMethod dexMethod = new DexEncodedMethod(method, flags,
-          createAnnotationSet(annotations), parameterAnnotationSets, code);
+          createAnnotationSet(annotations), annotationsList, code);
       if (flags.isStatic() || flags.isConstructor() || flags.isPrivate()) {
         parent.directMethods.add(dexMethod);
       } else {
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index dc75307..3bd4cde 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -104,46 +104,48 @@
   }
 
   @Override
-  public IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public IRCode buildIR(DexEncodedMethod encodedMethod, AppInfo appInfo,
+      InternalOptions options, Origin origin)
       throws ApiLevelException {
     triggerDelayedParsingIfNeccessary();
     return options.debug
-        ? internalBuildWithLocals(encodedMethod, options, null, null)
-        : internalBuild(encodedMethod, options, null, null);
+        ? internalBuildWithLocals(encodedMethod, appInfo, options, null, null)
+        : internalBuild(encodedMethod, appInfo, options, null, null);
   }
 
   @Override
   public IRCode buildInliningIR(
       DexEncodedMethod encodedMethod,
-      InternalOptions options,
+      AppInfo appInfo, InternalOptions options,
       ValueNumberGenerator generator,
-      Position callerPosition)
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
     assert generator != null;
     triggerDelayedParsingIfNeccessary();
     return options.debug
-        ? internalBuildWithLocals(encodedMethod, options, generator, callerPosition)
-        : internalBuild(encodedMethod, options, generator, callerPosition);
+        ? internalBuildWithLocals(encodedMethod, appInfo, options, generator, callerPosition)
+        : internalBuild(encodedMethod, appInfo, options, generator, callerPosition);
   }
 
   private IRCode internalBuildWithLocals(
       DexEncodedMethod encodedMethod,
-      InternalOptions options,
+      AppInfo appInfo, InternalOptions options,
       ValueNumberGenerator generator,
       Position callerPosition)
       throws ApiLevelException {
     try {
-      return internalBuild(encodedMethod, options, generator, callerPosition);
+      return internalBuild(encodedMethod, appInfo, options, generator, callerPosition);
     } catch (InvalidDebugInfoException e) {
       options.warningInvalidDebugInfo(encodedMethod, origin, e);
       node.localVariables.clear();
-      return internalBuild(encodedMethod, options, generator, callerPosition);
+      return internalBuild(encodedMethod, appInfo, options, generator, callerPosition);
     }
   }
 
   private IRCode internalBuild(
       DexEncodedMethod encodedMethod,
-      InternalOptions options,
+      AppInfo appInfo, InternalOptions options,
       ValueNumberGenerator generator,
       Position callerPosition)
       throws ApiLevelException {
@@ -154,8 +156,8 @@
         method.getHolder(), node, application, encodedMethod.method, callerPosition);
     IRBuilder builder =
         (generator == null)
-            ? new IRBuilder(encodedMethod, source, options)
-            : new IRBuilder(encodedMethod, source, options, generator);
+            ? new IRBuilder(encodedMethod, appInfo, source, options)
+            : new IRBuilder(encodedMethod, appInfo, source, options, generator);
     return builder.build();
   }
 
@@ -185,15 +187,44 @@
   }
 
   private void triggerDelayedParsingIfNeccessary() {
-    if (context != null) {
-      // The SecondVistor is in charge of setting the context to null.
-      DexProgramClass owner = context.owner;
-      new ClassReader(context.classCache).accept(new SecondVisitor(context, application),
-          ClassReader.SKIP_FRAMES);
-      assert verifyNoReparseContext(owner);
+    if (this.context != null) {
+      // The SecondVisitor is in charge of setting this.context to null.
+      ReparseContext context = this.context;
+      parseCode(context, false);
+      if (hasJsr(context)) {
+        System.out.println("JarCode: JSR encountered; reparse using JSRInlinerAdapter");
+        parseCode(context, true);
+        assert !hasJsr(context);
+      }
+      assert verifyNoReparseContext(context.owner);
     }
   }
 
+  private void parseCode(ReparseContext context, boolean useJsrInliner) {
+    SecondVisitor classVisitor = new SecondVisitor(context, application, useJsrInliner);
+    new ClassReader(context.classCache).accept(classVisitor, ClassReader.SKIP_FRAMES);
+  }
+
+  private boolean hasJsr(ReparseContext context) {
+    for (Code code : context.codeList) {
+      if (hasJsr(code.asJarCode().node)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean hasJsr(MethodNode node) {
+    Iterator<AbstractInsnNode> it = node.instructions.iterator();
+    while (it.hasNext()) {
+      int opcode = it.next().getOpcode();
+      if (opcode == Opcodes.JSR || opcode == Opcodes.RET) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * Fills the MethodNodes of all the methods in the class and removes the ReparseContext.
    */
@@ -201,18 +232,24 @@
 
     private final ReparseContext context;
     private final JarApplicationReader application;
+    private final boolean useJsrInliner;
     private int methodIndex = 0;
 
-    public SecondVisitor(ReparseContext context, JarApplicationReader application) {
+    public SecondVisitor(
+        ReparseContext context, JarApplicationReader application, boolean useJsrInliner) {
       super(Opcodes.ASM6);
       this.context = context;
       this.application = application;
+      this.useJsrInliner = useJsrInliner;
     }
 
     @Override
     public MethodVisitor visitMethod(int access, String name, String desc, String signature,
         String[] exceptions) {
-      MethodNode node = new JSRInlinerAdapter(null, access, name, desc, signature, exceptions);
+      MethodNode node =
+          useJsrInliner
+              ? new JSRInlinerAdapter(null, access, name, desc, signature, exceptions)
+              : new MethodNode(Opcodes.ASM6, access, name, desc, signature, exceptions);
       JarCode code = null;
       MethodAccessFlags flags = JarClassFileReader.createMethodAccessFlags(name, access);
       if (!flags.isAbstract() && !flags.isNative()) {
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index a864aba..bd6d3f7 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -64,6 +64,7 @@
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -78,6 +79,12 @@
 
 public class LazyCfCode extends Code {
 
+  private static class JsrEncountered extends RuntimeException {
+    public JsrEncountered(String s) {
+      super(s);
+    }
+  }
+
   public LazyCfCode(
       DexMethod method, Origin origin, ReparseContext context, JarApplicationReader application) {
 
@@ -107,17 +114,34 @@
   @Override
   public CfCode asCfCode() {
     if (code == null) {
+      ReparseContext context = this.context;
       assert context != null;
-      // The SecondVistor is in charge of setting the context to null.
-      DexProgramClass owner = context.owner;
-      ClassReader classReader = new ClassReader(context.classCache);
-      classReader.accept(new ClassCodeVisitor(context, application), ClassReader.EXPAND_FRAMES);
-      assert verifyNoReparseContext(owner);
+      // The ClassCodeVisitor is in charge of setting this.context to null.
+      try {
+        parseCode(context, false);
+      } catch (JsrEncountered e) {
+        System.out.println("LazyCfCode: JSR encountered; reparse using JSRInlinerAdapter");
+        for (Code code : context.codeList) {
+          code.asLazyCfCode().code = null;
+          code.asLazyCfCode().context = context;
+        }
+        try {
+          parseCode(context, true);
+        } catch (JsrEncountered e1) {
+          throw new Unreachable(e1);
+        }
+      }
+      assert verifyNoReparseContext(context.owner);
     }
     assert code != null;
     return code;
   }
 
+  public void parseCode(ReparseContext context, boolean useJsrInliner) {
+    ClassCodeVisitor classVisitor = new ClassCodeVisitor(context, application, useJsrInliner);
+    new ClassReader(context.classCache).accept(classVisitor, ClassReader.EXPAND_FRAMES);
+  }
+
   private void setCode(CfCode code) {
     assert this.code == null;
     assert this.context != null;
@@ -141,19 +165,33 @@
   }
 
   @Override
-  public IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public int estimatedSizeForInlining() {
+    return asCfCode().estimatedSizeForInlining();
+  }
+
+  @Override
+  public boolean estimatedSizeForInliningAtMost(int threshold) {
+    return asCfCode().estimatedSizeForInliningAtMost(threshold);
+  }
+
+  @Override
+  public IRCode buildIR(DexEncodedMethod encodedMethod, AppInfo appInfo,
+      InternalOptions options, Origin origin)
       throws ApiLevelException {
-    return asCfCode().buildIR(encodedMethod, options);
+    return asCfCode().buildIR(encodedMethod, appInfo, options, origin);
   }
 
   @Override
   public IRCode buildInliningIR(
       DexEncodedMethod encodedMethod,
+      AppInfo appInfo,
       InternalOptions options,
       ValueNumberGenerator valueNumberGenerator,
-      Position callerPosition)
+      Position callerPosition,
+      Origin origin)
       throws ApiLevelException {
-    return asCfCode().buildInliningIR(encodedMethod, options, valueNumberGenerator, callerPosition);
+    return asCfCode().buildInliningIR(
+        encodedMethod, appInfo, options, valueNumberGenerator, callerPosition, origin);
   }
 
   @Override
@@ -176,11 +214,14 @@
     private final ReparseContext context;
     private final JarApplicationReader application;
     private int methodIndex = 0;
+    private boolean usrJsrInliner;
 
-    ClassCodeVisitor(ReparseContext context, JarApplicationReader application) {
+    ClassCodeVisitor(
+        ReparseContext context, JarApplicationReader application, boolean useJsrInliner) {
       super(Opcodes.ASM6);
       this.context = context;
       this.application = application;
+      this.usrJsrInliner = useJsrInliner;
     }
 
     @Override
@@ -192,6 +233,9 @@
         DexMethod method = application.getMethod(context.owner.type, name, desc);
         assert code.method == method;
         MethodCodeVisitor methodVisitor = new MethodCodeVisitor(application, code);
+        if (!usrJsrInliner) {
+          return methodVisitor;
+        }
         return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
       }
       return null;
@@ -206,6 +250,7 @@
     private List<CfInstruction> instructions;
     private List<CfTryCatch> tryCatchRanges;
     private List<LocalVariableInfo> localVariables;
+    private final Map<DebugLocalInfo, DebugLocalInfo> canonicalDebugLocalInfo = new HashMap<>();
     private Map<Label, CfLabel> labelMap;
     private final LazyCfCode code;
     private DexMethod method;
@@ -302,7 +347,7 @@
 
     private DexType createTypeFromInternalType(String local) {
       assert local.indexOf('.') == -1;
-      return factory.createType("L" + local + ";");
+      return factory.createType(Type.getObjectType(local).getDescriptor());
     }
 
     @Override
@@ -567,7 +612,7 @@
           type = ValueType.OBJECT;
           break;
         case Opcodes.RET:
-          throw new Unreachable("RET should be handled by the ASM jsr inliner");
+          throw new JsrEncountered("RET should be handled by the ASM jsr inliner");
         default:
           throw new Unreachable("Unexpected VarInsn opcode: " + opcode);
       }
@@ -656,7 +701,7 @@
             instructions.add(new CfIf(type, ValueType.OBJECT, target));
             break;
           case Opcodes.JSR:
-            throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+            throw new JsrEncountered("JSR should be handled by the ASM jsr inliner");
           default:
             throw new Unreachable("Unexpected JumpInsn opcode: " + opcode);
         }
@@ -768,14 +813,19 @@
     public void visitLocalVariable(
         String name, String desc, String signature, Label start, Label end, int index) {
       DebugLocalInfo debugLocalInfo =
-          new DebugLocalInfo(
-              factory.createString(name),
-              factory.createType(desc),
-              signature == null ? null : factory.createString(signature));
+          canonicalize(
+              new DebugLocalInfo(
+                  factory.createString(name),
+                  factory.createType(desc),
+                  signature == null ? null : factory.createString(signature)));
       localVariables.add(
           new LocalVariableInfo(index, debugLocalInfo, getLabel(start), getLabel(end)));
     }
 
+    private DebugLocalInfo canonicalize(DebugLocalInfo debugLocalInfo) {
+      return canonicalDebugLocalInfo.computeIfAbsent(debugLocalInfo, o -> debugLocalInfo);
+    }
+
     @Override
     public void visitLineNumber(int line, Label start) {
       instructions.add(new CfPosition(getLabel(start), new Position(line, null, method, null)));
diff --git a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
new file mode 100644
index 0000000..23d5a38
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
@@ -0,0 +1,184 @@
+// 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.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.dex.IndexedItemCollection;
+import com.android.tools.r8.dex.MixedSectionCollection;
+import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * List of parameter annotations.
+ *
+ * <p>Due to a javac bug that went unfixed for multiple Java versions, the JVM specification does
+ * not require that the number of entries in the ParameterAnnotations attribute of a method matches
+ * the number of parameters in the method prototype; the number of ParameterAnnotations entries may
+ * be less than the number of prototype parameters for methods on inner classes.
+ *
+ * <p>There are two ways of accessing the parameter annotations:
+ *
+ * <ul>
+ *   <li>Using {@link ParameterAnnotationsList#forEachAnnotation(Consumer)}
+ *   <li>Using {@link ParameterAnnotationsList#size()}, {@link
+ *       ParameterAnnotationsList#isMissing(int)} and {@link ParameterAnnotationsList#get(int)}
+ * </ul>
+ *
+ * <p>The {@link ParameterAnnotationsList#forEachAnnotation(Consumer)} method visits all the {@link
+ * DexAnnotation}s specified in the ParameterAnnotations attribute. In contrast, the {@link
+ * ParameterAnnotationsList#size()} and {@link ParameterAnnotationsList#get(int)} methods may be
+ * used to access the annotations on individual parameters; these methods automatically shift
+ * parameter annotations up to mitigate the javac bug. The {@link
+ * ParameterAnnotationsList#isMissing(int)} accessor is used to determine whether a given parameter
+ * is missing in the ParameterAnnotations attribute.
+ */
+public class ParameterAnnotationsList extends DexItem {
+
+  private static final ParameterAnnotationsList EMPTY_PARAMETER_ANNOTATIONS_LIST =
+      new ParameterAnnotationsList();
+
+  private final DexAnnotationSet[] values;
+  private final int missingParameterAnnotations;
+
+  public static ParameterAnnotationsList empty() {
+    return EMPTY_PARAMETER_ANNOTATIONS_LIST;
+  }
+
+  private ParameterAnnotationsList() {
+    this.values = new DexAnnotationSet[0];
+    this.missingParameterAnnotations = 0;
+  }
+
+  public ParameterAnnotationsList(DexAnnotationSet[] values) {
+    this(values, 0);
+  }
+
+  public ParameterAnnotationsList(DexAnnotationSet[] values, int missingParameterAnnotations) {
+    assert values != null && values.length > 0;
+    this.values = values;
+    this.missingParameterAnnotations = missingParameterAnnotations;
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(values);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other instanceof ParameterAnnotationsList) {
+      return Arrays.equals(values, ((ParameterAnnotationsList) other).values);
+    }
+    return false;
+  }
+
+  @Override
+  public void collectIndexedItems(
+      IndexedItemCollection indexedItems, DexMethod method, int instructionOffset) {
+    collectAll(indexedItems, values);
+  }
+
+  @Override
+  void collectMixedSectionItems(MixedSectionCollection mixedItems) {
+    // Collect values first so that the annotation sets have sorted themselves before adding this.
+    collectAll(mixedItems, values);
+    mixedItems.add(this);
+  }
+
+  public boolean isEmpty() {
+    return values.length == 0;
+  }
+
+  /** Iterate over the {@link DexAnnotation}s of all parameters. */
+  public void forEachAnnotation(Consumer<DexAnnotation> consumer) {
+    for (DexAnnotationSet parameterAnnotations : values) {
+      for (DexAnnotation annotation : parameterAnnotations.annotations) {
+        consumer.accept(annotation);
+      }
+    }
+  }
+
+  /**
+   * Return the number of parameters in the method prototype, or zero if the method's parameters
+   * have no annotations.
+   */
+  public int size() {
+    return missingParameterAnnotations + values.length;
+  }
+
+  /**
+   * Return the number of parameters specified in the ParameterAnnotations attribute, that is, the
+   * number of parameters for which {@link ParameterAnnotationsList#isMissing(int)} returns false.
+   */
+  public int countNonMissing() {
+    return values.length;
+  }
+
+  /**
+   * Return true if the ParameterAnnotations attribute is missing an entry for this parameter. This
+   * is sometimes the case for the first parameter in a method on an inner class.
+   *
+   * @param i Index of the parameter in the method prototype.
+   */
+  public boolean isMissing(int i) {
+    assert i >= 0;
+    return i < missingParameterAnnotations;
+  }
+
+  /**
+   * Return the annotations on the {@code i}th parameter (indexed according to the method
+   * prototype). If the parameter's annotation list is missing, or {@code i} is not less than the
+   * number of parameters (see {@link ParameterAnnotationsList#isMissing(int)}), {@link
+   * DexAnnotationSet#empty()} is returned.
+   *
+   * @param i Index of the parameter in the method prototype.
+   */
+  public DexAnnotationSet get(int i) {
+    assert i >= 0;
+    int adjustedIndex = i - missingParameterAnnotations;
+    return (0 <= adjustedIndex && adjustedIndex < values.length)
+        ? values[adjustedIndex]
+        : DexAnnotationSet.empty();
+  }
+
+  /** Return a ParameterAnnotationsList extended to the given number of parameters. */
+  public ParameterAnnotationsList withParameterCount(int parameterCount) {
+    assert parameterCount >= size();
+    if (this == EMPTY_PARAMETER_ANNOTATIONS_LIST || parameterCount == size()) {
+      return this;
+    }
+    return new ParameterAnnotationsList(values, parameterCount - values.length);
+  }
+
+  /**
+   * Return a new ParameterAnnotationsList that keeps only the annotations matched by {@code
+   * filter}.
+   */
+  public ParameterAnnotationsList keepIf(Predicate<DexAnnotation> filter) {
+    DexAnnotationSet[] filtered = null;
+    boolean allEmpty = true;
+    for (int i = 0; i < values.length; i++) {
+      DexAnnotationSet updated = values[i].keepIf(filter);
+      if (updated != values[i]) {
+        if (filtered == null) {
+          filtered = values.clone();
+        }
+        filtered[i] = updated;
+      }
+      if (!updated.isEmpty()) {
+        allEmpty = false;
+      }
+    }
+    if (filtered == null) {
+      return this;
+    }
+    if (allEmpty) {
+      return ParameterAnnotationsList.empty();
+    }
+    return new ParameterAnnotationsList(filtered, missingParameterAnnotations);
+  }
+}
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 d59df07..113d522 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
@@ -424,6 +424,10 @@
     this.number = number;
   }
 
+  public String getNumberAsString() {
+    return number >= 0 ? "" + number : "<unknown>";
+  }
+
   public int numberInstructions(int nextInstructionNumber) {
     for (Instruction instruction : instructions) {
       instruction.setNumber(nextInstructionNumber);
@@ -826,7 +830,7 @@
       StringBuilder builder, List<BasicBlock> list, Function<BasicBlock, String> postfix) {
     if (list.size() > 0) {
       for (BasicBlock block : list) {
-        builder.append(block.number >= 0 ? block.number : "<unknown>");
+        builder.append(block.getNumberAsString());
         builder.append(postfix.apply(block));
         builder.append(' ');
       }
@@ -901,16 +905,15 @@
     int lineColumn = 0;
     int numberColumn = 0;
     for (Instruction instruction : instructions) {
-      lineColumn = Math.max(lineColumn, instruction.getPosition().toString().length());
+      lineColumn = Math.max(lineColumn, instruction.getPositionAsString().length());
       numberColumn = Math.max(numberColumn, digits(instruction.getNumber()));
     }
-    Position currentPosition = null;
+    String currentPosition = null;
     for (Instruction instruction : instructions) {
       if (lineColumn > 0) {
         String line = "";
-        if (!instruction.getPosition().equals(currentPosition)) {
-          currentPosition = instruction.getPosition();
-          line = currentPosition.toString();
+        if (!instruction.getPositionAsString().equals(currentPosition)) {
+          line = currentPosition = instruction.getPositionAsString();
         }
         StringUtils.appendLeftPadded(builder, line, lineColumn + 1);
         builder.append(": ");
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index b763298..cd5fcc8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -65,7 +65,7 @@
   @Override
   public String toString() {
     if (getBlock() != null && !getBlock().getSuccessors().isEmpty()) {
-      return super.toString() + "block " + blockNumberToString(getTarget());
+      return super.toString() + "block " + getTarget().getNumberAsString();
     }
     return super.toString() + "block <unknown>";
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index bb13495..1ec23d0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -358,6 +358,15 @@
     return true;
   }
 
+  public boolean hasCatchHandlers() {
+    for (BasicBlock block : blocks) {
+      if (block.hasCatchHandlers()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   private boolean consistentDefUseChains() {
     Set<Value> values = new HashSet<>();
 
@@ -578,16 +587,23 @@
   }
 
   public List<Value> collectArguments() {
+    return collectArguments(false);
+  }
+
+  public List<Value> collectArguments(boolean ignoreReceiver) {
     final List<Value> arguments = new ArrayList<>();
     Iterator<Instruction> iterator = blocks.get(0).iterator();
     while (iterator.hasNext()) {
       Instruction instruction = iterator.next();
       if (instruction.isArgument()) {
-        arguments.add(instruction.asArgument().outValue());
+        Value out = instruction.asArgument().outValue();
+        if (!ignoreReceiver || !out.isThis()) {
+          arguments.add(out);
+        }
       }
     }
     assert arguments.size()
-        == method.method.getArity() + (method.accessFlags.isStatic() ? 0 : 1);
+        == method.method.getArity() + ((method.accessFlags.isStatic() || ignoreReceiver) ? 0 : 1);
     return arguments;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java
index 0235a14..2e1609d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/If.java
+++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -130,9 +130,9 @@
         + " "
         + type
         + " block "
-        + blockNumberToString(getTrueTarget())
+        + getTrueTarget().getNumberAsString()
         + " (fallthrough "
-        + blockNumberToString(fallthroughBlock())
+        + fallthroughBlock().getNumberAsString()
         + ")";
   }
 
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 1c3423c..999699c 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
@@ -72,6 +72,10 @@
     this.position = position;
   }
 
+  public String getPositionAsString() {
+    return position == null ? "???" : position.toString();
+  }
+
   public List<Value> inValues() {
     return inValues;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
index d280dfe..81d4cdc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokePolymorphic.java
@@ -131,6 +131,6 @@
 
   @Override
   public InlineAction computeInlining(InliningOracle decider, DexType invocationContext) {
-    return decider.computeForInvokePolymorpic(this, invocationContext);
+    return decider.computeForInvokePolymorphic(this, invocationContext);
   }
 }
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 349d5df..db151c6 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
@@ -51,11 +51,4 @@
     return Constraint.ALWAYS;
   }
 
-  static String blockNumberToString(BasicBlock block) {
-    try {
-      return "" + block.getNumber();
-    } catch (AssertionError e) {
-      return "<invalid>";
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Switch.java b/src/main/java/com/android/tools/r8/ir/code/Switch.java
index 50e59f1..871500c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Switch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Switch.java
@@ -266,7 +266,7 @@
       builder.append("          ");
       builder.append(getKey(i));
       builder.append(" -> ");
-      builder.append(blockNumberToString(targetBlock(i)));
+      builder.append(targetBlock(i).getNumberAsString());
       builder.append("\n");
     }
     builder.append("          F -> ");
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
new file mode 100644
index 0000000..382a0a9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -0,0 +1,627 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.conversion;
+
+import static it.unimi.dsi.fastutil.ints.Int2ObjectSortedMaps.emptyMap;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+import com.android.tools.r8.cf.code.CfGoto;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.cf.code.CfSwitch;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CfState.Snapshot;
+import com.android.tools.r8.ir.conversion.IRBuilder.BlockInfo;
+import com.android.tools.r8.origin.Origin;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceSet;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CfSourceCode implements SourceCode {
+
+  private BlockInfo currentBlockInfo;
+
+  private static class TryHandlerList {
+
+    public final int startOffset;
+    public final int endOffset;
+    public final List<DexType> guards;
+    public final IntList offsets;
+
+    TryHandlerList(int startOffset, int endOffset, List<DexType> guards, IntList offsets) {
+      this.startOffset = startOffset;
+      this.endOffset = endOffset;
+      this.guards = guards;
+      this.offsets = offsets;
+    }
+
+    boolean validFor(int instructionOffset) {
+      return startOffset <= instructionOffset && instructionOffset < endOffset;
+    }
+
+    boolean isEmpty() {
+      assert guards.isEmpty() == offsets.isEmpty();
+      return guards.isEmpty();
+    }
+
+    static TryHandlerList computeTryHandlers(
+        int instructionOffset,
+        List<CfTryCatch> tryCatchRanges,
+        Reference2IntMap<CfLabel> labelOffsets) {
+      int startOffset = Integer.MIN_VALUE;
+      int endOffset = Integer.MAX_VALUE;
+      List<DexType> guards = new ArrayList<>();
+      IntList offsets = new IntArrayList();
+      ReferenceSet<DexType> seen = new ReferenceOpenHashSet<>();
+      for (CfTryCatch tryCatch : tryCatchRanges) {
+        int start = labelOffsets.getInt(tryCatch.start);
+        int end = labelOffsets.getInt(tryCatch.end);
+        if (start > instructionOffset) {
+          endOffset = Math.min(endOffset, start);
+          continue;
+        } else if (instructionOffset >= end) {
+          startOffset = Math.max(startOffset, end);
+          continue;
+        }
+        startOffset = Math.max(startOffset, start);
+        endOffset = Math.min(endOffset, end);
+        boolean seenCatchAll = false;
+        for (int i = 0; i < tryCatch.guards.size() && !seenCatchAll; i++) {
+          DexType guard = tryCatch.guards.get(i);
+          if (seen.add(guard)) {
+            guards.add(guard);
+            offsets.add(labelOffsets.getInt(tryCatch.targets.get(i)));
+            seenCatchAll = guard == DexItemFactory.catchAllType;
+          }
+        }
+        if (seenCatchAll) {
+          break;
+        }
+      }
+      return new TryHandlerList(startOffset, endOffset, guards, offsets);
+    }
+  }
+
+  private static class LocalVariableList {
+
+    public static final LocalVariableList EMPTY = new LocalVariableList(0, 0, emptyMap());
+    public final int startOffset;
+    public final int endOffset;
+    public final Int2ObjectMap<DebugLocalInfo> locals;
+
+    private LocalVariableList(
+        int startOffset, int endOffset, Int2ObjectMap<DebugLocalInfo> locals) {
+      this.startOffset = startOffset;
+      this.endOffset = endOffset;
+      this.locals = locals;
+    }
+
+    static LocalVariableList compute(
+        int instructionOffset,
+        List<CfCode.LocalVariableInfo> locals,
+        Reference2IntMap<CfLabel> labelOffsets) {
+      int startOffset = Integer.MIN_VALUE;
+      int endOffset = Integer.MAX_VALUE;
+      Int2ObjectMap<DebugLocalInfo> currentLocals = null;
+      for (LocalVariableInfo local : locals) {
+        int start = labelOffsets.getInt(local.getStart());
+        int end = labelOffsets.getInt(local.getEnd());
+        if (start > instructionOffset) {
+          endOffset = Math.min(endOffset, start);
+          continue;
+        } else if (instructionOffset >= end) {
+          startOffset = Math.max(startOffset, end);
+          continue;
+        }
+        if (currentLocals == null) {
+          currentLocals = new Int2ObjectOpenHashMap<>();
+        }
+        startOffset = Math.max(startOffset, start);
+        endOffset = Math.min(endOffset, end);
+        currentLocals.put(local.getIndex(), local.getLocal());
+      }
+      return new LocalVariableList(
+          startOffset, endOffset, currentLocals == null ? emptyMap() : currentLocals);
+    }
+
+    boolean validFor(int instructionOffset) {
+      return startOffset <= instructionOffset && instructionOffset < endOffset;
+    }
+
+    public DebugLocalInfo getLocal(int register) {
+      return locals.get(register);
+    }
+
+    public Int2ObjectOpenHashMap<DebugLocalInfo> merge(LocalVariableList other) {
+      return merge(this, other);
+    }
+
+    private static Int2ObjectOpenHashMap<DebugLocalInfo> merge(
+        LocalVariableList a, LocalVariableList b) {
+      if (a.locals.size() > b.locals.size()) {
+        return merge(b, a);
+      }
+      Int2ObjectOpenHashMap<DebugLocalInfo> result = new Int2ObjectOpenHashMap<>();
+      for (Entry<DebugLocalInfo> local : a.locals.int2ObjectEntrySet()) {
+        if (local.getValue().equals(b.getLocal(local.getIntKey()))) {
+          result.put(local.getIntKey(), local.getValue());
+        }
+      }
+      return result;
+    }
+  }
+
+  private CfState state;
+  private final CfCode code;
+  private final DexEncodedMethod method;
+  private final Position callerPosition;
+  private final Origin origin;
+
+  // Synthetic position with line = 0.
+  private final Position preamblePosition;
+  private final Reference2IntMap<CfLabel> labelOffsets = new Reference2IntOpenHashMap<>();
+  private TryHandlerList cachedTryHandlerList;
+  private LocalVariableList cachedLocalVariableList;
+  private int currentInstructionIndex;
+  private boolean inPrelude;
+  private Int2ObjectMap<DebugLocalInfo> incomingLocals;
+  private Int2ObjectMap<DebugLocalInfo> outgoingLocals;
+  private Int2ReferenceMap<Int2ObjectMap<DebugLocalInfo>> definitelyLiveIncomingLocals =
+      new Int2ReferenceOpenHashMap<>();
+  private Int2ReferenceMap<CfState.Snapshot> incomingState = new Int2ReferenceOpenHashMap<>();
+
+  public CfSourceCode(
+      CfCode code, DexEncodedMethod method, Position callerPosition, Origin origin) {
+    this.code = code;
+    this.method = method;
+    this.callerPosition = callerPosition;
+    this.origin = origin;
+    preamblePosition = Position.synthetic(0, method.method, null);
+    for (int i = 0; i < code.getInstructions().size(); i++) {
+      CfInstruction instruction = code.getInstructions().get(i);
+      if (instruction instanceof CfLabel) {
+        labelOffsets.put((CfLabel) instruction, instructionOffset(i));
+      }
+    }
+    this.state = new CfState(origin);
+  }
+
+  @Override
+  public int instructionCount() {
+    return code.getInstructions().size();
+  }
+
+  @Override
+  public int instructionIndex(int instructionOffset) {
+    return instructionOffset;
+  }
+
+  @Override
+  public int instructionOffset(int instructionIndex) {
+    return instructionIndex;
+  }
+
+  @Override
+  public boolean verifyRegister(int register) {
+    return true;
+  }
+
+  @Override
+  public void setUp() {}
+
+  @Override
+  public void clear() {}
+
+  @Override
+  public int traceInstruction(int instructionIndex, IRBuilder builder) {
+    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    if (instruction.canThrow()) {
+      TryHandlerList tryHandlers = getTryHandlers(instructionIndex);
+      if (!tryHandlers.isEmpty()) {
+        // Ensure the block starts at the start of the try-range (don't enqueue, not a target).
+        builder.ensureBlockWithoutEnqueuing(tryHandlers.startOffset);
+        IntSet seen = new IntOpenHashSet();
+        for (int offset : tryHandlers.offsets) {
+          if (seen.add(offset)) {
+            builder.ensureExceptionalSuccessorBlock(instructionIndex, offset);
+          }
+        }
+        if (!(instruction instanceof CfThrow)) {
+          builder.ensureNormalSuccessorBlock(instructionIndex, instructionIndex + 1);
+        }
+        return instructionIndex;
+      }
+      // If the throwable instruction is "throw" it closes the block.
+      return (instruction instanceof CfThrow) ? instructionIndex : -1;
+    }
+    if (isControlFlow(instruction)) {
+      for (int target : getTargets(instructionIndex)) {
+        builder.ensureNormalSuccessorBlock(instructionIndex, target);
+      }
+      return instructionIndex;
+    }
+    return -1;
+  }
+
+  private TryHandlerList getTryHandlers(int instructionOffset) {
+    if (cachedTryHandlerList == null || !cachedTryHandlerList.validFor(instructionOffset)) {
+      cachedTryHandlerList =
+          TryHandlerList.computeTryHandlers(
+              instructionOffset, code.getTryCatchRanges(), labelOffsets);
+    }
+    return cachedTryHandlerList;
+  }
+
+  private LocalVariableList getLocalVariables(int instructionOffset) {
+    if (cachedLocalVariableList == null || !cachedLocalVariableList.validFor(instructionOffset)) {
+      cachedLocalVariableList =
+          LocalVariableList.compute(instructionOffset, code.getLocalVariables(), labelOffsets);
+    }
+    return cachedLocalVariableList;
+  }
+
+  private int[] getTargets(int instructionIndex) {
+    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    assert isControlFlow(instruction);
+    CfLabel target = instruction.getTarget();
+    if (instruction.isReturn() || instruction instanceof CfThrow) {
+      assert target == null;
+      return new int[] {};
+    }
+    assert instruction instanceof CfSwitch || target != null
+        : "getTargets(): Non-control flow instruction " + instruction.getClass();
+    if (instruction instanceof CfSwitch) {
+      CfSwitch cfSwitch = (CfSwitch) instruction;
+      List<CfLabel> targets = cfSwitch.getSwitchTargets();
+      int[] res = new int[targets.size() + 1];
+      for (int i = 0; i < targets.size(); i++) {
+        res[i] = labelOffsets.getInt(targets.get(i));
+      }
+      res[targets.size()] = labelOffsets.getInt(cfSwitch.getDefaultTarget());
+      return res;
+    }
+    int targetIndex = labelOffsets.getInt(target);
+    if (instruction instanceof CfGoto) {
+      return new int[] {targetIndex};
+    }
+    assert instruction.isConditionalJump();
+    return new int[] {instructionIndex + 1, targetIndex};
+  }
+
+  @Override
+  public void buildPrelude(IRBuilder builder) {
+    assert !inPrelude;
+    inPrelude = true;
+    state.buildPrelude();
+    setLocalVariableLists();
+    buildArgumentInstructions(builder);
+    recordStateForTarget(0, state.getSnapshot());
+    // TODO: addDebugLocalUninitialized + addDebugLocalStart for non-argument locals live at 0
+    // TODO: Generate method synchronization
+    inPrelude = false;
+  }
+
+  private void buildArgumentInstructions(IRBuilder builder) {
+    int argumentRegister = 0;
+    if (!isStatic()) {
+      DexType type = method.method.holder;
+      state.write(argumentRegister, type);
+      builder.addThisArgument(argumentRegister++);
+    }
+    for (DexType type : method.method.proto.parameters.values) {
+      state.write(argumentRegister, type);
+      if (type.isBooleanType()) {
+        builder.addBooleanNonThisArgument(argumentRegister++);
+      } else {
+        ValueType valueType = ValueType.fromDexType(type);
+        builder.addNonThisArgument(argumentRegister, valueType);
+        argumentRegister += valueType.requiredRegisters();
+      }
+    }
+  }
+
+  private boolean isStatic() {
+    return method.accessFlags.isStatic();
+  }
+
+  @Override
+  public void buildPostlude(IRBuilder builder) {
+    // Since we're generating classfiles, we never need to synthesize monitor enter/exit.
+  }
+
+  @Override
+  public void buildInstruction(
+      IRBuilder builder, int instructionIndex, boolean firstBlockInstruction)
+      throws ApiLevelException {
+    CfInstruction instruction = code.getInstructions().get(instructionIndex);
+    currentInstructionIndex = instructionIndex;
+    if (firstBlockInstruction) {
+      currentBlockInfo = builder.getCFG().get(instructionIndex);
+      state.reset(incomingState.get(instructionIndex), instructionIndex == 0);
+    }
+    setLocalVariableLists();
+    readEndingLocals(builder);
+    if (isControlFlow(instruction)) {
+      ensureDebugValueLivenessControl(builder);
+      instruction.buildIR(builder, state, this);
+      Snapshot stateSnapshot = state.getSnapshot();
+      for (int target : getTargets(instructionIndex)) {
+        recordStateForTarget(target, stateSnapshot);
+      }
+      state.clear();
+    } else {
+      if (currentBlockInfo != null && instruction.canThrow()) {
+        Snapshot exceptionTransfer =
+            state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
+        for (int target : currentBlockInfo.exceptionalSuccessors) {
+          recordStateForTarget(target, exceptionTransfer);
+        }
+      }
+      instruction.buildIR(builder, state, this);
+      ensureDebugValueLiveness(builder);
+      if (builder.getCFG().containsKey(currentInstructionIndex + 1)) {
+        recordStateForTarget(currentInstructionIndex + 1, state.getSnapshot());
+      }
+    }
+  }
+
+  private void recordStateForTarget(int target, Snapshot snapshot) {
+    Snapshot existing = incomingState.get(target);
+    Snapshot merged = CfState.merge(existing, snapshot, origin);
+    if (merged != existing) {
+      incomingState.put(target, merged);
+    }
+  }
+
+  public int getCurrentInstructionIndex() {
+    return currentInstructionIndex;
+  }
+
+  public int getLabelOffset(CfLabel label) {
+    assert labelOffsets.containsKey(label);
+    return labelOffsets.getInt(label);
+  }
+
+  public void setStateFromFrame(CfFrame frame) {
+    Int2ReferenceSortedMap<FrameType> frameLocals = frame.getLocals();
+    DexType[] locals = new DexType[frameLocals.isEmpty() ? 0 : frameLocals.lastIntKey() + 1];
+    DexType[] stack = new DexType[frame.getStack().size()];
+    for (Int2ReferenceMap.Entry<FrameType> entry : frameLocals.int2ReferenceEntrySet()) {
+      locals[entry.getIntKey()] = convertUninitialized(entry.getValue());
+    }
+    for (int i = 0; i < stack.length; i++) {
+      stack[i] = convertUninitialized(frame.getStack().get(i));
+    }
+    state.setStateFromFrame(locals, stack, getDebugPositionAtOffset(currentInstructionIndex));
+  }
+
+  private DexType convertUninitialized(FrameType type) {
+    if (type.isInitialized()) {
+      return type.getInitializedType();
+    }
+    if (type.isUninitializedNew()) {
+      int labelOffset = getLabelOffset(type.getUninitializedLabel());
+      int insnOffset = labelOffset + 1;
+      while (insnOffset < code.getInstructions().size()) {
+        CfInstruction instruction = code.getInstructions().get(insnOffset);
+        if (!(instruction instanceof CfLabel)
+            && !(instruction instanceof CfFrame)
+            && !(instruction instanceof CfPosition)) {
+          assert instruction instanceof CfNew;
+          break;
+        }
+        insnOffset += 1;
+      }
+      CfInstruction instruction = code.getInstructions().get(insnOffset);
+      assert instruction instanceof CfNew;
+      return ((CfNew) instruction).getType();
+    }
+    if (type.isUninitializedThis()) {
+      return method.method.holder;
+    }
+    assert type.isTop();
+    return null;
+  }
+
+  @Override
+  public void resolveAndBuildSwitch(
+      int value, int fallthroughOffset, int payloadOffset, IRBuilder builder) {}
+
+  @Override
+  public void resolveAndBuildNewArrayFilledData(
+      int arrayRef, int payloadOffset, IRBuilder builder) {}
+
+  @Override
+  public DebugLocalInfo getIncomingLocal(int register) {
+    return incomingLocals.get(register);
+  }
+
+  @Override
+  public DebugLocalInfo getOutgoingLocal(int register) {
+    if (inPrelude) {
+      return getIncomingLocal(register);
+    }
+    assert !isControlFlow(code.getInstructions().get(currentInstructionIndex))
+        : "Outgoing local is undefined for control-flow instructions";
+    return outgoingLocals.get(register);
+  }
+
+  private void setLocalVariableLists() {
+    incomingLocals = getLocalVariables(currentInstructionIndex).locals;
+    CfInstruction currentInstruction = code.getInstructions().get(currentInstructionIndex);
+    if (inPrelude) {
+      outgoingLocals = incomingLocals;
+      return;
+    }
+    if (currentInstruction.isReturn() || currentInstruction instanceof CfThrow) {
+      outgoingLocals = emptyMap();
+      return;
+    }
+    if (isControlFlow(currentInstruction)) {
+      // We need to read all locals that are not live on all successors to ensure liveness.
+      // Determine outgoingLocals as the intersection of all successors' locals.
+      outgoingLocals = null;
+      int[] targets = getTargets(currentInstructionIndex);
+      for (int target : targets) {
+        Int2ObjectMap<DebugLocalInfo> locals = getLocalVariables(target).locals;
+        outgoingLocals = intersectMaps(outgoingLocals, locals);
+      }
+      assert outgoingLocals != null;
+      // Pass outgoingLocals to all successors.
+      for (int target : targets) {
+        Int2ObjectMap<DebugLocalInfo> existing = definitelyLiveIncomingLocals.get(target);
+        definitelyLiveIncomingLocals.put(target, intersectMaps(existing, outgoingLocals));
+      }
+    } else {
+      outgoingLocals = getLocalVariables(currentInstructionIndex + 1).locals;
+    }
+  }
+
+  private void readEndingLocals(IRBuilder builder) {
+    if (!outgoingLocals.equals(incomingLocals)) {
+      // Add reads of locals ending after the current instruction.
+      for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
+        if (!entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
+          builder.addDebugLocalRead(entry.getIntKey(), entry.getValue());
+        }
+      }
+    }
+  }
+
+  private Int2ObjectMap<DebugLocalInfo> intersectMaps(
+      Int2ObjectMap<DebugLocalInfo> existing, Int2ObjectMap<DebugLocalInfo> update) {
+    assert update != null;
+    if (existing == null) {
+      return update;
+    }
+    if (existing.size() > update.size()) {
+      return intersectMaps(update, existing);
+    }
+    if (existing.equals(update)) {
+      return existing;
+    }
+    Int2ObjectOpenHashMap<DebugLocalInfo> result = new Int2ObjectOpenHashMap<>();
+    for (Entry<DebugLocalInfo> local : existing.int2ObjectEntrySet()) {
+      if (local.getValue().equals(update.get(local.getIntKey()))) {
+        result.put(local.getIntKey(), local.getValue());
+      }
+    }
+    return result;
+  }
+
+  private void ensureDebugValueLiveness(IRBuilder builder) {
+    if (incomingLocals.equals(outgoingLocals)) {
+      return;
+    }
+    for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
+      if (entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
+        continue;
+      }
+      builder.addDebugLocalEnd(entry.getIntKey(), entry.getValue());
+    }
+    for (Entry<DebugLocalInfo> entry : outgoingLocals.int2ObjectEntrySet()) {
+      if (entry.getValue().equals(incomingLocals.get(entry.getIntKey()))) {
+        continue;
+      }
+      builder.addDebugLocalStart(entry.getIntKey(), entry.getValue());
+    }
+  }
+
+  private void ensureDebugValueLivenessControl(IRBuilder builder) {
+    if (incomingLocals.equals(outgoingLocals)) {
+      return;
+    }
+    for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
+      if (entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
+        continue;
+      }
+      builder.addDebugLocalRead(entry.getIntKey(), entry.getValue());
+    }
+    assert outgoingLocals
+        .int2ObjectEntrySet()
+        .stream()
+        .allMatch(entry -> entry.getValue().equals(incomingLocals.get(entry.getIntKey())));
+  }
+
+  private boolean isControlFlow(CfInstruction currentInstruction) {
+    return currentInstruction.isReturn()
+        || currentInstruction.getTarget() != null
+        || currentInstruction instanceof CfSwitch
+        || currentInstruction instanceof CfThrow;
+  }
+
+  @Override
+  public CatchHandlers<Integer> getCurrentCatchHandlers() {
+    TryHandlerList tryHandlers = getTryHandlers(instructionOffset(currentInstructionIndex));
+    if (tryHandlers.isEmpty()) {
+      return null;
+    }
+    return new CatchHandlers<>(tryHandlers.guards, tryHandlers.offsets);
+  }
+
+  @Override
+  public int getMoveExceptionRegister() {
+    return CfState.Slot.STACK_OFFSET;
+  }
+
+  @Override
+  public boolean verifyCurrentInstructionCanThrow() {
+    return code.getInstructions().get(currentInstructionIndex).canThrow();
+  }
+
+  @Override
+  public boolean verifyLocalInScope(DebugLocalInfo local) {
+    return false;
+  }
+
+  @Override
+  public Position getDebugPositionAtOffset(int offset) {
+    while (offset + 1 < code.getInstructions().size()) {
+      CfInstruction insn = code.getInstructions().get(offset);
+      if (!(insn instanceof CfLabel) && !(insn instanceof CfFrame)) {
+        break;
+      }
+      offset += 1;
+    }
+    while (offset >= 0 && !(code.getInstructions().get(offset) instanceof CfPosition)) {
+      offset -= 1;
+    }
+    if (offset < 0) {
+      return Position.noneWithMethod(method.method, callerPosition);
+    }
+    return ((CfPosition) code.getInstructions().get(offset)).getPosition();
+  }
+
+  @Override
+  public Position getCurrentPosition() {
+    return state.getPosition();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfState.java b/src/main/java/com/android/tools/r8/ir/conversion/CfState.java
new file mode 100644
index 0000000..2ac8e60
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfState.java
@@ -0,0 +1,542 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.conversion;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.origin.Origin;
+
+public class CfState {
+
+  private abstract static class SlotType {
+
+    public abstract DexType getPrecise();
+
+    public abstract ValueType getImprecise();
+
+    private static class Precise extends SlotType {
+      private final DexType type;
+
+      Precise(DexType type) {
+        this.type = type;
+      }
+
+      @Override
+      public DexType getPrecise() {
+        return type;
+      }
+
+      @Override
+      public ValueType getImprecise() {
+        return ValueType.fromDexType(type);
+      }
+
+      @Override
+      public String toString() {
+        return "Precise(" + type + ")";
+      }
+    }
+
+    private static class Imprecise extends SlotType {
+
+      private final ValueType type;
+
+      Imprecise(ValueType type) {
+        this.type = type;
+      }
+
+      @Override
+      public DexType getPrecise() {
+        return null;
+      }
+
+      @Override
+      public ValueType getImprecise() {
+        return type;
+      }
+
+      @Override
+      public String toString() {
+        return "Imprecise(" + type + ")";
+      }
+    }
+  }
+
+  private final Origin origin;
+  private Snapshot current;
+
+  public CfState(Origin origin) {
+    this.origin = origin;
+  }
+
+  private static final int MAX_UPDATES = 4;
+
+  public void buildPrelude() {
+    current = new BaseSnapshot();
+  }
+
+  public void clear() {
+    current = null;
+  }
+
+  public void reset(Snapshot snapshot, boolean isMethodEntry) {
+    assert !isMethodEntry || snapshot != null : "Must have snapshot for method entry.";
+    current = snapshot;
+  }
+
+  public void setStateFromFrame(DexType[] locals, DexType[] stack, Position position) {
+    assert current == null || stackHeight() == stack.length;
+    current = new BaseSnapshot(locals, stack, position);
+  }
+
+  public void merge(Snapshot snapshot) {
+    if (current == null) {
+      current = snapshot == null ? new BaseSnapshot() : snapshot;
+    } else {
+      current = merge(current, snapshot, origin);
+    }
+  }
+
+  public Snapshot getSnapshot() {
+    return current;
+  }
+
+  public static Snapshot merge(Snapshot current, Snapshot update, Origin origin) {
+    assert update != null;
+    if (current == null) {
+      return update;
+    }
+    return merge(current.asBase(), update.asBase(), origin);
+  }
+
+  private static Snapshot merge(BaseSnapshot current, BaseSnapshot update, Origin origin) {
+    if (current.stack.length != update.stack.length) {
+      throw new CompilationError(
+          "Different stack heights at jump target: "
+              + current.stack.length
+              + " != "
+              + update.stack.length,
+          origin);
+    }
+    // At this point, JarState checks if `current` has special "NULL" or "BYTE/BOOL" types
+    // that `update` does not have, and if so it computes a refinement.
+    // For now, let's just check that we didn't mix single/wide/reference types.
+    for (int i = 0; i < current.stack.length; i++) {
+      ValueType currentType = current.stack[i].getImprecise();
+      ValueType updateType = update.stack[i].getImprecise();
+      if (!currentType.compatible(updateType)) {
+        throw new CompilationError(
+            "Incompatible types in stack position "
+                + i
+                + ": "
+                + current.stack[i]
+                + " and "
+                + update.stack[i],
+            origin);
+      }
+    }
+    // We could check that locals are compatible, but that doesn't make sense since locals can be
+    // dead at this point.
+    return current;
+  }
+
+  public int stackHeight() {
+    return current.stackHeight();
+  }
+
+  public Slot push(Slot fromSlot) {
+    return push(fromSlot.slotType);
+  }
+
+  public Slot push(DexType type) {
+    return push(new SlotType.Precise(type));
+  }
+
+  public Slot push(ValueType type) {
+    return push(new SlotType.Imprecise(type));
+  }
+
+  private Slot push(SlotType slotType) {
+    Push newSnapshot = new Push(this.current, slotType);
+    updateState(newSnapshot);
+    return current.peek();
+  }
+
+  private void updateState(Snapshot newSnapshot) {
+    current = newSnapshot.updates >= MAX_UPDATES ? new BaseSnapshot(newSnapshot) : newSnapshot;
+  }
+
+  public Slot pop() {
+    Slot top = current.peek();
+    updateState(new Pop(current));
+    return top;
+  }
+
+  public int[] popReverse(int count) {
+    int[] registers = new int[count];
+    for (int i = count - 1; i >= 0; i--) {
+      registers[i] = pop().register;
+    }
+    return registers;
+  }
+
+  public Slot peek() {
+    return current.peek();
+  }
+
+  public Slot peek(int skip) {
+    return current.getStack(current.stackHeight() - 1 - skip);
+  }
+
+  public Slot read(int localIndex) {
+    return current.getLocal(localIndex);
+  }
+
+  public Slot write(int localIndex, DexType type) {
+    return write(localIndex, new SlotType.Precise(type));
+  }
+
+  public Slot write(int localIndex, Slot src) {
+    return write(localIndex, src.slotType);
+  }
+
+  private Slot write(int localIndex, SlotType slotType) {
+    updateState(new Write(current, localIndex, slotType));
+    return current.getLocal(localIndex);
+  }
+
+  public Position getPosition() {
+    return current.getPosition();
+  }
+
+  public void setPosition(Position position) {
+    assert position != null;
+    updateState(new SetPosition(current, position));
+  }
+
+  @Override
+  public String toString() {
+    return new BaseSnapshot(current).toString();
+  }
+
+  public static class Slot {
+
+    public static final int STACK_OFFSET = 100000;
+    public final int register;
+    public final ValueType type;
+    public final DexType preciseType;
+    private final SlotType slotType;
+
+    private Slot(int register, DexType preciseType) {
+      this(register, new SlotType.Precise(preciseType));
+    }
+
+    private Slot(int register, SlotType type) {
+      this.register = register;
+      this.slotType = type;
+      this.type = type.getImprecise();
+      this.preciseType = type.getPrecise();
+    }
+
+    private static Slot stackSlot(int stackPosition, SlotType type) {
+      return new Slot(stackPosition + STACK_OFFSET, type);
+    }
+
+    private int stackPosition() {
+      assert register >= STACK_OFFSET;
+      return register - STACK_OFFSET;
+    }
+
+    @Override
+    public String toString() {
+      return register < STACK_OFFSET
+          ? register + "=" + slotType
+          : "s" + (register - STACK_OFFSET) + "=" + slotType;
+    }
+  }
+
+  public abstract static class Snapshot {
+    final Snapshot parent;
+    final int updates;
+
+    private Snapshot(Snapshot parent, int updates) {
+      this.parent = parent;
+      this.updates = updates;
+    }
+
+    public int stackHeight() {
+      return parent.stackHeight();
+    }
+
+    public int maxLocal() {
+      return parent.maxLocal();
+    }
+
+    public Slot getStack(int i) {
+      return parent.getStack(i);
+    }
+
+    public Slot peek() {
+      return parent.peek();
+    }
+
+    public Slot getLocal(int i) {
+      return parent.getLocal(i);
+    }
+
+    public Position getPosition() {
+      return parent.getPosition();
+    }
+
+    void build(BaseSnapshot base) {
+      parent.build(base);
+    }
+
+    BaseSnapshot asBase() {
+      return new BaseSnapshot(this);
+    }
+
+    public Snapshot exceptionTransfer(DexType throwableType) {
+      BaseSnapshot result = new BaseSnapshot(maxLocal() + 1, 1, getPosition());
+      build(result);
+      result.stack[0] = new SlotType.Precise(throwableType);
+      return result;
+    }
+  }
+
+  private static class BaseSnapshot extends Snapshot {
+    final SlotType[] locals;
+    final SlotType[] stack;
+    final Position position;
+
+    BaseSnapshot() {
+      this(0, 0, Position.none());
+    }
+
+    BaseSnapshot(int locals, int stack, Position position) {
+      super(null, 0);
+      assert position != null;
+      this.locals = new SlotType[locals];
+      this.stack = new SlotType[stack];
+      this.position = position;
+    }
+
+    BaseSnapshot(Snapshot newSnapshot) {
+      this(newSnapshot.maxLocal() + 1, newSnapshot.stackHeight(), newSnapshot.getPosition());
+      newSnapshot.build(this);
+    }
+
+    BaseSnapshot(DexType[] locals, DexType[] stack, Position position) {
+      super(null, 0);
+      assert position != null;
+      this.locals = new SlotType[locals.length];
+      this.stack = new SlotType[stack.length];
+      this.position = position;
+      for (int i = 0; i < locals.length; i++) {
+        this.locals[i] = locals[i] == null ? null : getSlotType(locals[i]);
+      }
+      for (int i = 0; i < stack.length; i++) {
+        assert stack[i] != null;
+        this.stack[i] = getSlotType(stack[i]);
+      }
+    }
+
+    private SlotType getSlotType(DexType local) {
+      return local.toDescriptorString().equals("NULL")
+          ? new SlotType.Imprecise(ValueType.OBJECT)
+          : new SlotType.Precise(local);
+    }
+
+    @Override
+    public int stackHeight() {
+      return stack.length;
+    }
+
+    @Override
+    public int maxLocal() {
+      return locals.length - 1;
+    }
+
+    @Override
+    public Slot getStack(int i) {
+      return Slot.stackSlot(i, stack[i]);
+    }
+
+    @Override
+    public Slot peek() {
+      assert stackHeight() > 0;
+      return getStack(stackHeight() - 1);
+    }
+
+    @Override
+    public Slot getLocal(int i) {
+      return new Slot(i, locals[i]);
+    }
+
+    @Override
+    public Position getPosition() {
+      return position;
+    }
+
+    @Override
+    void build(BaseSnapshot dest) {
+      for (int i = 0; i < locals.length && i < dest.locals.length; i++) {
+        dest.locals[i] = locals[i];
+      }
+      for (int i = 0; i < stack.length && i < dest.stack.length; i++) {
+        dest.stack[i] = stack[i];
+      }
+    }
+
+    @Override
+    BaseSnapshot asBase() {
+      return this;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder stringBuilder =
+          new StringBuilder().append("position: ").append(position).append(" stack: [");
+      String sep = "";
+      for (SlotType type : stack) {
+        stringBuilder.append(sep).append(type);
+        sep = ", ";
+      }
+      stringBuilder.append("] locals: [");
+      sep = "";
+      for (int i = 0; i < locals.length; i++) {
+        if (locals[i] != null) {
+          stringBuilder.append(sep).append(i).append(':').append(locals[i]);
+          sep = ", ";
+        }
+      }
+      return stringBuilder.append(']').toString();
+    }
+  }
+
+  private static class Push extends Snapshot {
+
+    private final Slot slot;
+
+    Push(Snapshot parent, SlotType type) {
+      super(parent, parent.updates + 1);
+      slot = Slot.stackSlot(parent.stackHeight(), type);
+      assert 100000 <= slot.register && slot.register < 200000;
+    }
+
+    @Override
+    public int stackHeight() {
+      return slot.stackPosition() + 1;
+    }
+
+    @Override
+    public Slot getStack(int i) {
+      return i == slot.stackPosition() ? peek() : parent.getStack(i);
+    }
+
+    @Override
+    public Slot peek() {
+      return slot;
+    }
+
+    @Override
+    void build(BaseSnapshot base) {
+      parent.build(base);
+      if (slot.stackPosition() < base.stack.length) {
+        base.stack[slot.stackPosition()] = slot.slotType;
+      }
+    }
+
+    @Override
+    public String toString() {
+      return parent.toString() + "; push(" + slot.slotType + ")";
+    }
+  }
+
+  private static class Pop extends Snapshot {
+
+    private final int stackHeight;
+
+    Pop(Snapshot parent) {
+      super(parent, parent.updates + 1);
+      stackHeight = parent.stackHeight() - 1;
+      assert stackHeight >= 0;
+    }
+
+    @Override
+    public int stackHeight() {
+      return stackHeight;
+    }
+
+    @Override
+    public Slot getStack(int i) {
+      assert i < stackHeight;
+      return parent.getStack(i);
+    }
+
+    @Override
+    public Slot peek() {
+      return parent.getStack(stackHeight - 1);
+    }
+
+    @Override
+    public String toString() {
+      return parent.toString() + "; pop";
+    }
+  }
+
+  private static class Write extends Snapshot {
+
+    private final Slot slot;
+
+    Write(Snapshot parent, int slotIndex, SlotType type) {
+      super(parent, parent.updates + 1);
+      slot = new Slot(slotIndex, type);
+      assert 0 <= slotIndex && slotIndex < 100000;
+    }
+
+    @Override
+    public int maxLocal() {
+      return Math.max(slot.register, parent.maxLocal());
+    }
+
+    @Override
+    public Slot getLocal(int i) {
+      return i == slot.register ? slot : parent.getLocal(i);
+    }
+
+    @Override
+    void build(BaseSnapshot base) {
+      parent.build(base);
+      base.locals[slot.register] = slot.slotType;
+    }
+
+    @Override
+    public String toString() {
+      return parent.toString() + "; write " + slot.register + " := " + slot.slotType;
+    }
+  }
+
+  private static class SetPosition extends Snapshot {
+
+    private final Position position;
+
+    SetPosition(Snapshot parent, Position position) {
+      super(parent, parent.updates + 1);
+      this.position = position;
+    }
+
+    @Override
+    public Position getPosition() {
+      return position;
+    }
+
+    @Override
+    public String toString() {
+      return parent.toString() + "; set pos " + position;
+    }
+  }
+}
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 58ed822..c697105 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
@@ -8,11 +8,13 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
 import com.android.tools.r8.graph.DexProto;
@@ -113,6 +115,10 @@
 
   public static final int INITIAL_BLOCK_OFFSET = -1;
 
+  public DexItemFactory getFactory() {
+    return options.itemFactory;
+  }
+
   // SSA construction uses a worklist of basic blocks reachable from the entry and their
   // instruction offsets.
   private static class WorklistItem {
@@ -240,6 +246,35 @@
       fallthroughInfo.exceptionalSuccessors = new IntArraySet(this.exceptionalSuccessors);
       return fallthroughInfo;
     }
+
+    @Override
+    public String toString() {
+      StringBuilder stringBuilder =
+          new StringBuilder()
+              .append("block ")
+              .append(block.getNumberAsString())
+              .append(" predecessors: ");
+      String sep = "";
+      for (int offset : normalPredecessors) {
+        stringBuilder.append(sep).append(offset);
+        sep = ", ";
+      }
+      for (int offset : exceptionalPredecessors) {
+        stringBuilder.append(sep).append('*').append(offset);
+        sep = ", ";
+      }
+      stringBuilder.append(" successors: ");
+      sep = "";
+      for (int offset : normalSuccessors) {
+        stringBuilder.append(sep).append(offset);
+        sep = ", ";
+      }
+      for (int offset : exceptionalSuccessors) {
+        stringBuilder.append(sep).append('*').append(offset);
+        sep = ", ";
+      }
+      return stringBuilder.toString();
+    }
   }
 
   // Mapping from instruction offsets to basic-block targets.
@@ -261,18 +296,15 @@
   private final LinkedList<BasicBlock> blocks = new LinkedList<>();
 
   private BasicBlock currentBlock = null;
-
   private final List<BasicBlock.Pair> needGotoToCatchBlocks = new ArrayList<>();
-
   final private ValueNumberGenerator valueNumberGenerator;
-
   private final DexEncodedMethod method;
+  private final AppInfo appInfo;
 
   // Source code to build IR from. Null if already built.
   private SourceCode source;
 
-  boolean throwingInstructionInCurrentBlock = false;
-
+  private boolean throwingInstructionInCurrentBlock = false;
   private final InternalOptions options;
 
   // Pending local reads.
@@ -281,16 +313,17 @@
 
   private int nextBlockNumber = 0;
 
-  public IRBuilder(DexEncodedMethod method, SourceCode source, InternalOptions options) {
-    this(method, source, options, new ValueNumberGenerator());
+  public IRBuilder(DexEncodedMethod method, AppInfo appInfo,
+      SourceCode source, InternalOptions options) {
+    this(method, appInfo, source, options, new ValueNumberGenerator());
   }
 
   public IRBuilder(
-      DexEncodedMethod method,
-      SourceCode source,
+      DexEncodedMethod method, AppInfo appInfo, SourceCode source,
       InternalOptions options, ValueNumberGenerator valueNumberGenerator) {
     assert source != null;
     this.method = method;
+    this.appInfo = appInfo;
     this.source = source;
     this.valueNumberGenerator = valueNumberGenerator;
     this.options = options;
@@ -304,6 +337,10 @@
     return targets;
   }
 
+  public DexMethod getMethod() {
+    return method.method;
+  }
+
   private void addToWorklist(BasicBlock block, int firstInstructionIndex) {
     // TODO(ager): Filter out the ones that are already in the worklist, mark bit in block?
     if (!block.isFilled()) {
@@ -615,6 +652,7 @@
     assert local != null;
     assert local == getOutgoingLocal(register);
     ValueType valueType = ValueType.fromDexType(local.type);
+    // TODO(mathiasr): Here we create a Phi with type based on debug info. That's just wrong!
     Value incomingValue = readRegisterIgnoreLocal(register, valueType);
 
     // TODO(mathiasr): This can be simplified once trivial phi removal is local-info aware.
@@ -1004,7 +1042,7 @@
   public void addInvoke(
       Type type, DexItem item, DexProto callSiteProto, List<Value> arguments, boolean itf)
       throws ApiLevelException {
-    if (type == Invoke.Type.POLYMORPHIC) {
+    if (type == Type.POLYMORPHIC) {
       assert item instanceof DexMethod;
       if (!options.canUseInvokePolymorphic()) {
         throw new ApiLevelException(
@@ -1019,6 +1057,19 @@
             null /* sourceString */);
       }
     }
+    if (appInfo != null && type == Type.VIRTUAL) {
+      // If an invoke-virtual targets a private method in the current class overriding will
+      // not apply (see jvm spec on method resolution 5.4.3.3 and overriding 5.4.5) and
+      // therefore we use an invoke-direct instead. We need to do this as the Android Runtime
+      // will not allow invoke-virtual of a private method.
+      DexMethod invocationMethod = (DexMethod) item;
+      if (invocationMethod.holder == method.method.holder) {
+        DexEncodedMethod directTarget = appInfo.lookupDirectTarget(invocationMethod);
+        if (directTarget != null && invocationMethod.holder == directTarget.method.holder) {
+          type = Type.DIRECT;
+        }
+      }
+    }
     add(Invoke.create(type, item, callSiteProto, null, arguments, itf));
   }
 
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 7e573f5..8477d9b 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
@@ -46,6 +46,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
@@ -87,6 +88,7 @@
   private final LambdaRewriter lambdaRewriter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final LambdaMerger lambdaMerger;
+  private final ClassInliner classInliner;
   private final InternalOptions options;
   private final CfgPrinter printer;
   private final GraphLense graphLense;
@@ -160,6 +162,9 @@
       this.identifierNameStringMarker = null;
       this.devirtualizer = null;
     }
+    this.classInliner =
+        (options.enableClassInlining && options.enableInlining && inliner != null)
+            ? new ClassInliner(appInfo.dexItemFactory) : null;
   }
 
   /**
@@ -578,7 +583,7 @@
       feedback.markProcessed(method, Constraint.NEVER);
       return;
     }
-    IRCode code = method.buildIR(options);
+    IRCode code = method.buildIR(appInfo, options, appInfo.originFor(method.method.holder));
     if (code == null) {
       feedback.markProcessed(method, Constraint.NEVER);
       return;
@@ -686,6 +691,15 @@
       assert code.isConsistentSSA();
     }
 
+    if (classInliner != null) {
+      assert options.enableInlining && inliner != null;
+      classInliner.processMethodCode(
+          appInfo.withSubtyping(), method, code, isProcessedConcurrently,
+          methodsToInline -> inliner.performForcedInlining(method, code, methodsToInline)
+      );
+      assert code.isConsistentSSA();
+    }
+
     if (options.outline.enabled) {
       outlineHandler.accept(code, method);
       assert code.isConsistentSSA();
@@ -705,6 +719,12 @@
       assert code.isConsistentSSA();
     }
 
+    // Analysis must be done after method is rewritten by logArgumentTypes()
+    codeRewriter.identifyReceiverOnlyUsedForReadingFields(method, code, feedback);
+    if (method.isInstanceInitializer()) {
+      codeRewriter.identifyOnlyInitializesFieldsWithNoOtherSideEffects(method, code, feedback);
+    }
+
     printMethod(code, "Optimized IR (SSA)");
     finalizeIR(method, code, feedback);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index b2c5cfd..26ca6b2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public interface OptimizationFeedback {
   void methodReturnsArgument(DexEncodedMethod method, int argument);
@@ -15,4 +18,7 @@
   void markProcessed(DexEncodedMethod method, Constraint state);
   void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
   void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
+  void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields);
+  void markOnlyInitializesFieldsWithNoOtherSideEffects(
+      DexEncodedMethod method, Map<DexField, Integer> mapping);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index ac1972b..645df64 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public class OptimizationFeedbackDirect implements OptimizationFeedback {
 
@@ -43,4 +46,15 @@
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
     method.markTriggerClassInitBeforeAnySideEffect(mark);
   }
+
+  @Override
+  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
+    method.markReceiverOnlyUsedForReadingFields(fields);
+  }
+
+  @Override
+  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
+      Map<DexField, Integer> mapping) {
+    method.markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 80cb04f..97fe052 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -5,7 +5,10 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.Map;
+import java.util.Set;
 
 public class OptimizationFeedbackIgnore implements OptimizationFeedback {
 
@@ -29,4 +32,13 @@
 
   @Override
   public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
+
+  @Override
+  public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
+  }
+
+  @Override
+  public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
+      Map<DexField, Integer> mapping) {
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index 7befc69..c75456d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -111,7 +111,7 @@
     // Some debuggers (like IntelliJ) automatically skip synthetic methods on single step.
     newFlags.setSynthetic();
     return new DexEncodedMethod(newMethod, newFlags,
-        defaultMethod.annotations, defaultMethod.parameterAnnotations,
+        defaultMethod.annotations, defaultMethod.parameterAnnotationsList,
         new SynthesizedCode(new ForwardMethodSourceCode(
             clazz.type, method.proto, /* static method */ null,
             rewriter.defaultAsMethodOfCompanionClass(method),
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index 898a2b2..1e8d57a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -70,7 +70,7 @@
             || (companionMethod.getArity() == dexCode.getDebugInfo().parameters.length);
 
         companionMethods.add(new DexEncodedMethod(companionMethod,
-            newFlags, virtual.annotations, virtual.parameterAnnotations, code));
+            newFlags, virtual.annotations, virtual.parameterAnnotationsList, code));
 
         // Make the method abstract.
         virtual.accessFlags.setAbstract();
@@ -106,7 +106,7 @@
             + "either be public or private in " + iface.origin;
         companionMethods.add(new DexEncodedMethod(
             rewriter.staticAsMethodOfCompanionClass(direct.method), newFlags,
-            direct.annotations, direct.parameterAnnotations, direct.getCode()));
+            direct.annotations, direct.parameterAnnotationsList, direct.getCode()));
 
       } else {
         if (originalFlags.isPrivate()) {
@@ -129,7 +129,7 @@
               || (companionMethod.getArity() == dexCode.getDebugInfo().parameters.length);
 
           companionMethods.add(new DexEncodedMethod(companionMethod,
-              newFlags, direct.annotations, direct.parameterAnnotations, code));
+              newFlags, direct.annotations, direct.parameterAnnotationsList, code));
 
         } else {
           // Since there are no interface constructors at this point,
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 82958da..d9bd4bd 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -10,7 +10,6 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -27,6 +26,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueNull;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.origin.SynthesizedOrigin;
@@ -171,7 +171,7 @@
             MethodAccessFlags.fromSharedAccessFlags(
                 Constants.ACC_PUBLIC | Constants.ACC_FINAL, false),
             DexAnnotationSet.empty(),
-            DexAnnotationSetRefList.empty(),
+            ParameterAnnotationsList.empty(),
             new SynthesizedCode(new LambdaMainMethodSourceCode(this, mainMethod)));
 
     // Synthesize bridge methods.
@@ -187,7 +187,7 @@
                       | Constants.ACC_BRIDGE,
                   false),
               DexAnnotationSet.empty(),
-              DexAnnotationSetRefList.empty(),
+              ParameterAnnotationsList.empty(),
               new SynthesizedCode(
                   new LambdaBridgeMethodSourceCode(this, mainMethod, bridgeMethod)));
     }
@@ -208,7 +208,7 @@
                     | Constants.ACC_SYNTHETIC,
                 true),
             DexAnnotationSet.empty(),
-            DexAnnotationSetRefList.empty(),
+            ParameterAnnotationsList.empty(),
             new SynthesizedCode(new LambdaConstructorSourceCode(this)));
 
     // Class constructor for stateless lambda classes.
@@ -219,7 +219,7 @@
               MethodAccessFlags.fromSharedAccessFlags(
                   Constants.ACC_SYNTHETIC | Constants.ACC_STATIC, true),
               DexAnnotationSet.empty(),
-              DexAnnotationSetRefList.empty(),
+              ParameterAnnotationsList.empty(),
               new SynthesizedCode(new LambdaClassConstructorSourceCode(this)));
     }
     return methods;
@@ -492,7 +492,7 @@
           // relax its accessibility without making it virtual.
           DexEncodedMethod newMethod = new DexEncodedMethod(
               callTarget, encodedMethod.accessFlags, encodedMethod.annotations,
-              encodedMethod.parameterAnnotations, encodedMethod.getCode());
+              encodedMethod.parameterAnnotationsList, encodedMethod.getCode());
           // TODO(ager): Should we give the new first parameter an actual name? Maybe 'this'?
           encodedMethod.accessFlags.setStatic();
           encodedMethod.accessFlags.unsetPrivate();
@@ -532,7 +532,7 @@
               Constants.ACC_SYNTHETIC | Constants.ACC_STATIC | Constants.ACC_PUBLIC,
               false);
       DexEncodedMethod accessorEncodedMethod = new DexEncodedMethod(
-          callTarget, accessorFlags, DexAnnotationSet.empty(), DexAnnotationSetRefList.empty(),
+          callTarget, accessorFlags, DexAnnotationSet.empty(), ParameterAnnotationsList.empty(),
           new SynthesizedCode(new AccessorMethodSourceCode(LambdaClass.this)));
       accessorClass.setDirectMethods(appendMethod(
           accessorClass.directMethods(), accessorEncodedMethod));
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index 5ea5d0e..45c08fb 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -126,7 +126,10 @@
 
       // `a` is primitive and `b` is a supertype of the boxed type `a`.
       DexType boxedPrimitiveType = getBoxedForPrimitiveType(a);
-      if (b == boxedPrimitiveType || b == factory.objectType) {
+      if (b == boxedPrimitiveType ||
+          b == factory.objectType ||
+          b == factory.serializableType ||
+          b == factory.comparableType) {
         return true;
       }
       return boxedPrimitiveType != factory.boxedCharType
@@ -337,6 +340,8 @@
       DexType boxedFromType = getBoxedForPrimitiveType(fromType);
       if (toType == boxedFromType ||
           toType == factory().objectType ||
+          toType == factory().serializableType ||
+          toType == factory().comparableType ||
           (boxedFromType != factory().booleanType &&
               boxedFromType != factory().charType &&
               toType == factory().boxedNumberType)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 85a3ac6..e5c8962 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -47,10 +47,13 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.If.Type;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeNewArray;
 import com.android.tools.r8.ir.code.InvokeVirtual;
@@ -733,6 +736,122 @@
     }
   }
 
+  public void identifyReceiverOnlyUsedForReadingFields(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    if (!method.isNonAbstractVirtualMethod()) {
+      return;
+    }
+
+    feedback.markReceiverOnlyUsedForReadingFields(method, null);
+
+    Value instance = code.getThis();
+    if (instance.numberOfPhiUsers() > 0) {
+      return;
+    }
+
+    Set<DexField> fields = Sets.newIdentityHashSet();
+    for (Instruction insn : instance.uniqueUsers()) {
+      if (!insn.isInstanceGet()) {
+        return;
+      }
+      InstanceGet get = insn.asInstanceGet();
+      if (get.dest() == instance || get.object() != instance) {
+        return;
+      }
+      fields.add(get.getField());
+    }
+    feedback.markReceiverOnlyUsedForReadingFields(method, fields);
+  }
+
+  public void identifyOnlyInitializesFieldsWithNoOtherSideEffects(
+      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+    assert method.isInstanceInitializer();
+
+    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, null);
+
+    if (code.hasCatchHandlers()) {
+      return;
+    }
+
+    List<Value> args = code.collectArguments(true /* not interested in receiver */);
+    Map<DexField, Integer> mapping = new IdentityHashMap<>();
+    Value receiver = code.getThis();
+
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction instruction = it.next();
+
+      // Mark an argument.
+      if (instruction.isArgument()) {
+        continue;
+      }
+
+      // Allow super call to java.lang.Object.<init>() on 'this'.
+      if (instruction.isInvokeDirect()) {
+        InvokeDirect invokedDirect = instruction.asInvokeDirect();
+        if (invokedDirect.getInvokedMethod() != dexItemFactory.objectMethods.constructor ||
+            invokedDirect.getReceiver() != receiver) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow final return.
+      if (instruction.isReturn()) {
+        continue;
+      }
+
+      // Allow assignment to this class' fields. If the assigned value is an argument
+      // reference update the mep. Otherwise just allow assigning any value, since all
+      // invalid values should be filtered out at the definitions.
+      if (instruction.isInstancePut()) {
+        InstancePut instancePut = instruction.asInstancePut();
+        DexField field = instancePut.getField();
+        if (instancePut.object() != receiver) {
+          return;
+        }
+
+        Value value = instancePut.value();
+        if (value.isArgument() && !value.isThis()) {
+          assert (args.contains(value));
+          mapping.put(field, args.indexOf(value));
+        } else {
+          mapping.remove(field);
+        }
+        continue;
+      }
+
+      // Allow non-throwing constants.
+      if (instruction.isConstInstruction()) {
+        if (instruction.instructionTypeCanThrow()) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow goto instructions jumping to the next block.
+      if (instruction.isGoto()) {
+        if (instruction.getBlock().getNumber() + 1 !=
+            instruction.asGoto().getTarget().getNumber()) {
+          return;
+        }
+        continue;
+      }
+
+      // Allow binary and unary instructions if they don't throw.
+      if (instruction.isBinop() || instruction.isUnop()) {
+        if (instruction.instructionTypeCanThrow()) {
+          return;
+        }
+        continue;
+      }
+
+      return;
+    }
+
+    feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, mapping);
+  }
+
   /**
    * An enum used to classify instructions according to a particular effect that they produce.
    *
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
new file mode 100644
index 0000000..90081d1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -0,0 +1,351 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokePolymorphic;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.logging.Log;
+import java.util.ListIterator;
+import java.util.function.Predicate;
+
+final class DefaultInliningOracle implements InliningOracle, InliningStrategy {
+
+  private final Inliner inliner;
+  private final DexEncodedMethod method;
+  private final IRCode code;
+  private final TypeEnvironment typeEnvironment;
+  private final CallSiteInformation callSiteInformation;
+  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
+  private final InliningInfo info;
+  private final int inliningInstructionLimit;
+  private int instructionAllowance;
+
+  DefaultInliningOracle(
+      Inliner inliner,
+      DexEncodedMethod method,
+      IRCode code,
+      TypeEnvironment typeEnvironment,
+      CallSiteInformation callSiteInformation,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      int inliningInstructionLimit,
+      int inliningInstructionAllowance) {
+    this.inliner = inliner;
+    this.method = method;
+    this.code = code;
+    this.typeEnvironment = typeEnvironment;
+    this.callSiteInformation = callSiteInformation;
+    this.isProcessedConcurrently = isProcessedConcurrently;
+    info = Log.ENABLED ? new InliningInfo(method) : null;
+    this.inliningInstructionLimit = inliningInstructionLimit;
+    this.instructionAllowance = inliningInstructionAllowance;
+  }
+
+  @Override
+  public void finish() {
+    if (Log.ENABLED) {
+      Log.debug(getClass(), info.toString());
+    }
+  }
+
+  private DexEncodedMethod validateCandidate(InvokeMethod invoke, DexType invocationContext) {
+    DexEncodedMethod candidate =
+        invoke.computeSingleTarget(inliner.appInfo, typeEnvironment, invocationContext);
+    if ((candidate == null)
+        || (candidate.getCode() == null)
+        || inliner.appInfo.definitionFor(candidate.method.getHolder()).isLibraryClass()) {
+      if (info != null) {
+        info.exclude(invoke, "No inlinee");
+      }
+      return null;
+    }
+    // Ignore the implicit receiver argument.
+    int numberOfArguments =
+        invoke.arguments().size() - (invoke.isInvokeMethodWithReceiver() ? 1 : 0);
+    if (numberOfArguments != candidate.method.getArity()) {
+      if (info != null) {
+        info.exclude(invoke, "Argument number mismatch");
+      }
+      return null;
+    }
+    return candidate;
+  }
+
+  private Reason computeInliningReason(DexEncodedMethod target) {
+    if (target.getOptimizationInfo().forceInline()) {
+      return Reason.FORCE;
+    }
+    if (inliner.appInfo.hasLiveness()
+        && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
+      return Reason.ALWAYS;
+    }
+    if (callSiteInformation.hasSingleCallSite(target)) {
+      return Reason.SINGLE_CALLER;
+    }
+    if (isDoubleInliningTarget(target)) {
+      return Reason.DUAL_CALLER;
+    }
+    return Reason.SIMPLE;
+  }
+
+  private boolean canInlineStaticInvoke(DexEncodedMethod method, DexEncodedMethod target) {
+    // Only proceed with inlining a static invoke if:
+    // - the holder for the target equals the holder for the method, or
+    // - the target method always triggers class initialization of its holder before any other side
+    //   effect (hence preserving class initialization semantics).
+    // - there is no non-trivial class initializer.
+    DexType targetHolder = target.method.getHolder();
+    if (method.method.getHolder() == targetHolder) {
+      return true;
+    }
+    DexClass clazz = inliner.appInfo.definitionFor(targetHolder);
+    assert clazz != null;
+    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
+      return true;
+    }
+    return classInitializationHasNoSideffects(targetHolder);
+  }
+
+  /**
+   * Check for class initializer side effects when loading this class, as inlining might remove the
+   * load operation.
+   * <p>
+   * See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
+   * <p>
+   * For simplicity, we are conservative and consider all interfaces, not only the ones with default
+   * methods.
+   */
+  private boolean classInitializationHasNoSideffects(DexType classToCheck) {
+    DexClass clazz = inliner.appInfo.definitionFor(classToCheck);
+    if ((clazz == null)
+        || clazz.hasNonTrivialClassInitializer()
+        || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
+      return false;
+    }
+    for (DexType iface : clazz.interfaces.values) {
+      if (!classInitializationHasNoSideffects(iface)) {
+        return false;
+      }
+    }
+    return clazz.superType == null || classInitializationHasNoSideffects(clazz.superType);
+  }
+
+  private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
+    // 10 is found from measuring.
+    return inliner.isDoubleInliningTarget(callSiteInformation, candidate)
+        && candidate.getCode().estimatedSizeForInliningAtMost(10);
+  }
+
+  private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
+      Reason reason) {
+    if (method == candidate) {
+      // Cannot handle recursive inlining at this point.
+      // Force inlined method should never be recursive.
+      assert !candidate.getOptimizationInfo().forceInline();
+      if (info != null) {
+        info.exclude(invoke, "direct recursion");
+      }
+      return false;
+    }
+
+    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "is processed in parallel");
+      }
+      return false;
+    }
+
+    // Abort inlining attempt if method -> target access is not right.
+    if (!inliner.hasInliningAccess(method, candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "target does not have right access");
+      }
+      return false;
+    }
+
+    DexClass holder = inliner.appInfo.definitionFor(candidate.method.getHolder());
+    if (holder.isInterface()) {
+      // Art978_virtual_interfaceTest correctly expects an IncompatibleClassChangeError exception at
+      // runtime.
+      if (info != null) {
+        info.exclude(invoke, "Do not inline target if method holder is an interface class");
+      }
+      return false;
+    }
+
+    if (holder.isLibraryClass()) {
+      // Library functions should not be inlined.
+      return false;
+    }
+
+    // Don't inline if target is synchronized.
+    if (candidate.accessFlags.isSynchronized()) {
+      if (info != null) {
+        info.exclude(invoke, "target is synchronized");
+      }
+      return false;
+    }
+
+    // Attempt to inline a candidate that is only called twice.
+    if ((reason == Reason.DUAL_CALLER) && (inliner.doubleInlining(method, candidate) == null)) {
+      if (info != null) {
+        info.exclude(invoke, "target is not ready for double inlining");
+      }
+      return false;
+    }
+
+    if (reason == Reason.SIMPLE) {
+      // If we are looking for a simple method, only inline if actually simple.
+      Code code = candidate.getCode();
+      if (!code.estimatedSizeForInliningAtMost(inliningInstructionLimit)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext) {
+    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
+    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+      return null;
+    }
+
+    // We can only inline an instance method call if we preserve the null check semantic (which
+    // would throw NullPointerException if the receiver is null). Therefore we can inline only if
+    // one of the following conditions is true:
+    // * the candidate inlinee checks null receiver before any side effect
+    // * the receiver is known to be non-null
+    boolean receiverIsNeverNull =
+        !typeEnvironment.getLatticeElement(invoke.getReceiver()).isNullable();
+    if (!receiverIsNeverNull
+        && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
+      if (info != null) {
+        info.exclude(invoke, "receiver for candidate can be null");
+      }
+      return null;
+    }
+
+    Reason reason = computeInliningReason(candidate);
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+      // Abort inlining attempt if the single target is not an inlining candidate.
+      if (info != null) {
+        info.exclude(invoke, "target is not identified for inlining");
+      }
+      return null;
+    }
+
+    if (!passesInliningConstraints(invoke, candidate, reason)) {
+      return null;
+    }
+
+    if (info != null) {
+      info.include(invoke.getType(), candidate);
+    }
+    return new InlineAction(candidate, invoke, reason);
+  }
+
+  @Override
+  public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
+    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
+    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+      return null;
+    }
+
+    Reason reason = computeInliningReason(candidate);
+    // Determine if this should be inlined no matter how big it is.
+    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
+      // Abort inlining attempt if the single target is not an inlining candidate.
+      if (info != null) {
+        info.exclude(invoke, "target is not identified for inlining");
+      }
+      return null;
+    }
+
+    // Abort inlining attempt if we can not guarantee class for static target has been initialized.
+    if (!canInlineStaticInvoke(method, candidate)) {
+      if (info != null) {
+        info.exclude(invoke, "target is static but we cannot guarantee class has been initialized");
+      }
+      return null;
+    }
+
+    if (!passesInliningConstraints(invoke, candidate, reason)) {
+      return null;
+    }
+
+    if (info != null) {
+      info.include(invoke.getType(), candidate);
+    }
+    return new InlineAction(candidate, invoke, reason);
+  }
+
+  @Override
+  public InlineAction computeForInvokePolymorphic(
+      InvokePolymorphic invoke, DexType invocationContext) {
+    // TODO: No inlining of invoke polymorphic for now.
+    if (info != null) {
+      info.exclude(invoke, "inlining through invoke signature polymorpic is not supported");
+    }
+    return null;
+  }
+
+  @Override
+  public void ensureMethodProcessed(
+      DexEncodedMethod target, IRCode inlinee) throws ApiLevelException {
+    if (!target.isProcessed()) {
+      if (Log.ENABLED) {
+        Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
+      }
+      inliner.performInlining(
+          target, inlinee, typeEnvironment, isProcessedConcurrently, callSiteInformation);
+    }
+  }
+
+  @Override
+  public boolean exceededAllowance() {
+    return instructionAllowance < 0;
+  }
+
+  @Override
+  public void markInlined(IRCode inlinee) {
+    instructionAllowance -= inliner.numberOfInstructions(inlinee);
+  }
+
+  @Override
+  public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
+    if (inliner.options.enableNonNullTracking) {
+      // Move the cursor back to where the inlinee blocks are added.
+      blockIterator = code.blocks.listIterator(code.blocks.indexOf(block));
+      // Kick off the tracker to add non-null IRs only to the inlinee blocks.
+      new NonNullTracker()
+          .addNonNullInPart(code, blockIterator, inlinee.blocks::contains);
+      // Move the cursor forward to where the inlinee blocks end.
+      blockIterator = code.blocks.listIterator(code.blocks.indexOf(invokeSuccessor));
+    }
+    // Update type env for inlined blocks.
+    typeEnvironment.analyzeBlocks(inlinee.topologicallySortedBlocks());
+    // TODO(b/69964136): need a test where refined env in inlinee affects the caller.
+    return blockIterator;
+  }
+
+  @Override
+  public DexType getReceiverTypeIfKnown(InvokeMethod invoke) {
+    return null; // Maybe improve later.
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java
index 31fc38f..59ddc36 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/EnumOrdinalMapCollector.java
@@ -56,7 +56,7 @@
       return;
     }
     DexEncodedMethod initializer = clazz.getClassInitializer();
-    IRCode code = initializer.getCode().buildIR(initializer, options);
+    IRCode code = initializer.getCode().buildIR(initializer, appInfo, options, clazz.origin);
     Reference2IntMap<DexField> ordinalsMap = new Reference2IntArrayMap<>();
     ordinalsMap.defaultReturnValue(-1);
     InstructionIterator it = code.instructionIterator();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
new file mode 100644
index 0000000..86d40d9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokePolymorphic;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import java.util.ListIterator;
+import java.util.Map;
+
+final class ForcedInliningOracle implements InliningOracle, InliningStrategy {
+  private final DexEncodedMethod method;
+  private final Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline;
+
+  ForcedInliningOracle(DexEncodedMethod method,
+      Map<InvokeMethodWithReceiver, Inliner.InliningInfo> invokesToInline) {
+    this.method = method;
+    this.invokesToInline = invokesToInline;
+  }
+
+  @Override
+  public void finish() {
+  }
+
+  @Override
+  public InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext) {
+    Inliner.InliningInfo info = invokesToInline.get(invoke);
+    if (info == null) {
+      return null;
+    }
+
+    assert method != info.target;
+    return new InlineAction(info.target, invoke, Reason.FORCE);
+  }
+
+  @Override
+  public InlineAction computeForInvokeStatic(
+      InvokeStatic invoke, DexType invocationContext) {
+    return null; // Not yet supported.
+  }
+
+  @Override
+  public InlineAction computeForInvokePolymorphic(
+      InvokePolymorphic invoke, DexType invocationContext) {
+    return null; // Not yet supported.
+  }
+
+  @Override
+  public void ensureMethodProcessed(DexEncodedMethod target, IRCode inlinee) {
+    assert target.isProcessed();
+  }
+
+  @Override
+  public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
+    return blockIterator;
+  }
+
+  @Override
+  public boolean exceededAllowance() {
+    return false; // Never exceeds allowance.
+  }
+
+  @Override
+  public void markInlined(IRCode inlinee) {
+  }
+
+  @Override
+  public DexType getReceiverTypeIfKnown(InvokeMethod invoke) {
+    assert invoke.isInvokeMethodWithReceiver();
+    Inliner.InliningInfo info = invokesToInline.get(invoke.asInvokeMethodWithReceiver());
+    assert info != null;
+    return info.receiverType;
+  }
+}
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 678ddae..860cc21 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
@@ -21,6 +21,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
@@ -28,7 +29,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.OptimizationFeedback;
-import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Sets;
@@ -42,10 +43,11 @@
 import java.util.stream.Collectors;
 
 public class Inliner {
+  private static final int INITIAL_INLINING_INSTRUCTION_ALLOWANCE = 1500;
 
   protected final AppInfoWithLiveness appInfo;
   private final GraphLense graphLense;
-  private final InternalOptions options;
+  final InternalOptions options;
 
   // State for inlining methods which are known to be called twice.
   private boolean applyDoubleInlining = false;
@@ -268,7 +270,8 @@
         Position callerPosition)
         throws ApiLevelException {
       // Build the IR for a yet not processed method, and perform minimal IR processing.
-      IRCode code = target.buildInliningIR(options, generator, callerPosition);
+      Origin origin = appInfo.originFor(target.method.holder);
+      IRCode code = target.buildInliningIR(appInfo, options, generator, callerPosition, origin);
       if (!target.isProcessed()) {
         new LensCodeRewriter(graphLense, appInfo).rewrite(code, target);
       }
@@ -276,7 +279,7 @@
     }
   }
 
-  private int numberOfInstructions(IRCode code) {
+  final int numberOfInstructions(IRCode code) {
     int numOfInstructions = 0;
     for (BasicBlock block : code.blocks) {
       numOfInstructions += block.getInstructions().size();
@@ -362,6 +365,23 @@
     return true;
   }
 
+  public static class InliningInfo {
+    public final DexEncodedMethod target;
+    public final DexType receiverType; // null, if unknown
+
+    public InliningInfo(DexEncodedMethod target, DexType receiverType) {
+      this.target = target;
+      this.receiverType = receiverType;
+    }
+  }
+
+  public void performForcedInlining(DexEncodedMethod method, IRCode code,
+      Map<InvokeMethodWithReceiver, InliningInfo> invokesToInline) throws ApiLevelException {
+
+    ForcedInliningOracle oracle = new ForcedInliningOracle(method, invokesToInline);
+    performInliningImpl(oracle, oracle, method, code);
+  }
+
   public void performInlining(
       DexEncodedMethod method,
       IRCode code,
@@ -369,29 +389,40 @@
       Predicate<DexEncodedMethod> isProcessedConcurrently,
       CallSiteInformation callSiteInformation)
       throws ApiLevelException {
-    int instruction_allowance = 1500;
-    instruction_allowance -= numberOfInstructions(code);
-    if (instruction_allowance < 0) {
-      return;
-    }
-    InliningOracle oracle =
-        new InliningOracle(
+
+    DefaultInliningOracle oracle =
+        new DefaultInliningOracle(
             this,
             method,
+            code,
             typeEnvironment,
             callSiteInformation,
             isProcessedConcurrently,
-            options.inliningInstructionLimit);
+            options.inliningInstructionLimit,
+            INITIAL_INLINING_INSTRUCTION_ALLOWANCE - numberOfInstructions(code));
+
+    performInliningImpl(oracle, oracle, method, code);
+  }
+
+  private void performInliningImpl(
+      InliningStrategy strategy,
+      InliningOracle oracle,
+      DexEncodedMethod method,
+      IRCode code)
+      throws ApiLevelException {
+    if (strategy.exceededAllowance()) {
+      return;
+    }
 
     List<BasicBlock> blocksToRemove = new ArrayList<>();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
-    while (blockIterator.hasNext() && (instruction_allowance >= 0)) {
+    while (blockIterator.hasNext() && !strategy.exceededAllowance()) {
       BasicBlock block = blockIterator.next();
       if (blocksToRemove.contains(block)) {
         continue;
       }
       InstructionListIterator iterator = block.listIterator();
-      while (iterator.hasNext() && (instruction_allowance >= 0)) {
+      while (iterator.hasNext() && !strategy.exceededAllowance()) {
         Instruction current = iterator.next();
         if (current.isInvokeMethod()) {
           InvokeMethod invoke = current.asInvokeMethod();
@@ -414,49 +445,27 @@
               if (block.hasCatchHandlers() && inlinee.computeNormalExitBlocks().isEmpty()) {
                 continue;
               }
+
               // If this code did not go through the full pipeline, apply inlining to make sure
               // that force inline targets get processed.
-              if (!target.isProcessed()) {
-                assert result.reason == Reason.FORCE;
-                if (Log.ENABLED) {
-                  Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
-                }
-                performInlining(
-                    target, inlinee, typeEnvironment, isProcessedConcurrently, callSiteInformation);
-              }
+              strategy.ensureMethodProcessed(target, inlinee);
+
               // Make sure constructor inlining is legal.
               assert !target.isClassInitializer();
               if (target.isInstanceInitializer()
                   && !legalConstructorInline(method, invoke, inlinee)) {
                 continue;
               }
-              DexType downcast = null;
-              if (invoke.isInvokeMethodWithReceiver()) {
-                // If the invoke has a receiver but the declared method holder is different
-                // from the computed target holder, inlining requires a downcast of the receiver.
-                if (target.method.getHolder() != invoke.getInvokedMethod().getHolder()) {
-                  downcast = result.target.method.getHolder();
-                }
-              }
+              DexType downcast = createDowncastIfNeeded(strategy, invoke, target);
               // Inline the inlinee code in place of the invoke instruction
               // Back up before the invoke instruction.
               iterator.previous();
-              instruction_allowance -= numberOfInstructions(inlinee);
-              if (instruction_allowance >= 0 || result.ignoreInstructionBudget()) {
+              strategy.markInlined(inlinee);
+              if (!strategy.exceededAllowance() || result.ignoreInstructionBudget()) {
                 BasicBlock invokeSuccessor =
                     iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
-                if (options.enableNonNullTracking) {
-                  // Move the cursor back to where the inlinee blocks are added.
-                  blockIterator = code.blocks.listIterator(code.blocks.indexOf(block));
-                  // Kick off the tracker to add non-null IRs only to the inlinee blocks.
-                  new NonNullTracker()
-                      .addNonNullInPart(code, blockIterator, inlinee.blocks::contains);
-                  // Move the cursor forward to where the inlinee blocks end.
-                  blockIterator = code.blocks.listIterator(code.blocks.indexOf(invokeSuccessor));
-                }
-                // Update type env for inlined blocks.
-                typeEnvironment.analyzeBlocks(inlinee.topologicallySortedBlocks());
-                // TODO(b/69964136): need a test where refined env in inlinee affects the caller.
+                blockIterator = strategy.
+                    updateTypeInformationIfNeeded(inlinee, blockIterator, block, invokeSuccessor);
 
                 // If we inlined the invoke from a bridge method, it is no longer a bridge method.
                 if (method.accessFlags.isBridge()) {
@@ -479,4 +488,22 @@
     code.removeAllTrivialPhis();
     assert code.isConsistentSSA();
   }
+
+  private DexType createDowncastIfNeeded(
+      InliningStrategy strategy, InvokeMethod invoke, DexEncodedMethod target) {
+    if (invoke.isInvokeMethodWithReceiver()) {
+      // If the invoke has a receiver but the actual type of the receiver is different
+      // from the computed target holder, inlining requires a downcast of the receiver.
+      DexType assumedReceiverType = strategy.getReceiverTypeIfKnown(invoke);
+      if (assumedReceiverType == null) {
+        // In case we don't know exact type of the receiver we use declared
+        // method holder as a fallback.
+        assumedReceiverType = invoke.getInvokedMethod().getHolder();
+      }
+      if (assumedReceiverType != target.method.getHolder()) {
+        return target.method.getHolder();
+      }
+    }
+    return null;
+  }
 }
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 7947c01..08f8045 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
@@ -1,297 +1,28 @@
 // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
 package com.android.tools.r8.ir.optimize;
 
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.analysis.type.TypeEnvironment;
-import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokePolymorphic;
 import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.conversion.CallSiteInformation;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
-import com.android.tools.r8.ir.optimize.Inliner.Reason;
-import com.android.tools.r8.logging.Log;
-import java.util.function.Predicate;
 
 /**
- * The InliningOracle contains information needed for when inlining
- * other methods into @method.
+ * The InliningOracle contains information needed for when inlining other methods into @method.
  */
-public class InliningOracle {
+public interface InliningOracle {
 
-  private final Inliner inliner;
-  private final DexEncodedMethod method;
-  private final TypeEnvironment typeEnvironment;
-  private final CallSiteInformation callSiteInformation;
-  private final Predicate<DexEncodedMethod> isProcessedConcurrently;
-  private final InliningInfo info;
-  private final int inliningInstructionLimit;
+  void finish();
 
-  InliningOracle(
-      Inliner inliner,
-      DexEncodedMethod method,
-      TypeEnvironment typeEnvironment,
-      CallSiteInformation callSiteInformation,
-      Predicate<DexEncodedMethod> isProcessedConcurrently,
-      int inliningInstructionLimit) {
-    this.inliner = inliner;
-    this.method = method;
-    this.typeEnvironment = typeEnvironment;
-    this.callSiteInformation = callSiteInformation;
-    this.isProcessedConcurrently = isProcessedConcurrently;
-    info = Log.ENABLED ? new InliningInfo(method) : null;
-    this.inliningInstructionLimit = inliningInstructionLimit;
-  }
+  InlineAction computeForInvokeWithReceiver(
+      InvokeMethodWithReceiver invoke, DexType invocationContext);
 
-  void finish() {
-    if (Log.ENABLED) {
-      Log.debug(getClass(), info.toString());
-    }
-  }
+  InlineAction computeForInvokeStatic(
+      InvokeStatic invoke, DexType invocationContext);
 
-  private DexEncodedMethod validateCandidate(InvokeMethod invoke, DexType invocationContext) {
-    DexEncodedMethod candidate =
-        invoke.computeSingleTarget(inliner.appInfo, typeEnvironment, invocationContext);
-    if ((candidate == null)
-        || (candidate.getCode() == null)
-        || inliner.appInfo.definitionFor(candidate.method.getHolder()).isLibraryClass()) {
-      if (info != null) {
-        info.exclude(invoke, "No inlinee");
-      }
-      return null;
-    }
-    // Ignore the implicit receiver argument.
-    int numberOfArguments =
-        invoke.arguments().size() - (invoke.isInvokeMethodWithReceiver() ? 1 : 0);
-    if (numberOfArguments != candidate.method.getArity()) {
-      if (info != null) {
-        info.exclude(invoke, "Argument number mismatch");
-      }
-      return null;
-    }
-    return candidate;
-  }
-
-  private Reason computeInliningReason(DexEncodedMethod target) {
-    if (target.getOptimizationInfo().forceInline()) {
-      return Reason.FORCE;
-    }
-    if (inliner.appInfo.hasLiveness()
-        && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
-      return Reason.ALWAYS;
-    }
-    if (callSiteInformation.hasSingleCallSite(target)) {
-      return Reason.SINGLE_CALLER;
-    }
-    if (isDoubleInliningTarget(target)) {
-      return Reason.DUAL_CALLER;
-    }
-    return Reason.SIMPLE;
-  }
-
-  private boolean canInlineStaticInvoke(DexEncodedMethod method, DexEncodedMethod target) {
-    // Only proceed with inlining a static invoke if:
-    // - the holder for the target equals the holder for the method, or
-    // - the target method always triggers class initialization of its holder before any other side
-    //   effect (hence preserving class initialization semantics).
-    // - there is no non-trivial class initializer.
-    DexType targetHolder = target.method.getHolder();
-    if (method.method.getHolder() == targetHolder) {
-      return true;
-    }
-    DexClass clazz = inliner.appInfo.definitionFor(targetHolder);
-    assert clazz != null;
-    if (target.getOptimizationInfo().triggersClassInitBeforeAnySideEffect()) {
-      return true;
-    }
-    return classInitializationHasNoSideffects(targetHolder);
-  }
-
-  /**
-   * Check for class initializer side effects when loading this class, as inlining might remove the
-   * load operation.
-   * <p>
-   * See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-5.html#jvms-5.5.
-   * <p>
-   * For simplicity, we are conservative and consider all interfaces, not only the ones with default
-   * methods.
-   */
-  private boolean classInitializationHasNoSideffects(DexType classToCheck) {
-    DexClass clazz = inliner.appInfo.definitionFor(classToCheck);
-    if ((clazz == null)
-        || clazz.hasNonTrivialClassInitializer()
-        || clazz.defaultValuesForStaticFieldsMayTriggerAllocation()) {
-      return false;
-    }
-    for (DexType iface : clazz.interfaces.values) {
-      if (!classInitializationHasNoSideffects(iface)) {
-        return false;
-      }
-    }
-    return clazz.superType == null || classInitializationHasNoSideffects(clazz.superType);
-  }
-
-  private synchronized boolean isDoubleInliningTarget(DexEncodedMethod candidate) {
-    // 10 is found from measuring.
-    return inliner.isDoubleInliningTarget(callSiteInformation, candidate)
-        && candidate.getCode().isDexCode()
-        && (candidate.getCode().asDexCode().instructions.length <= 10);
-  }
-
-  private boolean passesInliningConstraints(InvokeMethod invoke, DexEncodedMethod candidate,
-      Reason reason) {
-    if (method == candidate) {
-      // Cannot handle recursive inlining at this point.
-      // Force inlined method should never be recursive.
-      assert !candidate.getOptimizationInfo().forceInline();
-      if (info != null) {
-        info.exclude(invoke, "direct recursion");
-      }
-      return false;
-    }
-
-    if (reason != Reason.FORCE && isProcessedConcurrently.test(candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "is processed in parallel");
-      }
-      return false;
-    }
-
-    // Abort inlining attempt if method -> target access is not right.
-    if (!inliner.hasInliningAccess(method, candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "target does not have right access");
-      }
-      return false;
-    }
-
-    DexClass holder = inliner.appInfo.definitionFor(candidate.method.getHolder());
-    if (holder.isInterface()) {
-      // Art978_virtual_interfaceTest correctly expects an IncompatibleClassChangeError exception at
-      // runtime.
-      if (info != null) {
-        info.exclude(invoke, "Do not inline target if method holder is an interface class");
-      }
-      return false;
-    }
-
-    if (holder.isLibraryClass()) {
-      // Library functions should not be inlined.
-      return false;
-    }
-
-    // Don't inline if target is synchronized.
-    if (candidate.accessFlags.isSynchronized()) {
-      if (info != null) {
-        info.exclude(invoke, "target is synchronized");
-      }
-      return false;
-    }
-
-    // Attempt to inline a candidate that is only called twice.
-    if ((reason == Reason.DUAL_CALLER) && (inliner.doubleInlining(method, candidate) == null)) {
-      if (info != null) {
-        info.exclude(invoke, "target is not ready for double inlining");
-      }
-      return false;
-    }
-
-    if (reason == Reason.SIMPLE) {
-      // If we are looking for a simple method, only inline if actually simple.
-      Code code = candidate.getCode();
-      if (code.estimatedSizeForInlining() > inliningInstructionLimit) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public InlineAction computeForInvokeWithReceiver(
-      InvokeMethodWithReceiver invoke, DexType invocationContext) {
-    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
-      return null;
-    }
-
-    // We can only inline an instance method call if we preserve the null check semantic (which
-    // would throw NullPointerException if the receiver is null). Therefore we can inline only if
-    // one of the following conditions is true:
-    // * the candidate inlinee checks null receiver before any side effect
-    // * the receiver is known to be non-null
-    boolean receiverIsNeverNull =
-        !typeEnvironment.getLatticeElement(invoke.getReceiver()).isNullable();
-    if (!receiverIsNeverNull
-        && !candidate.getOptimizationInfo().checksNullReceiverBeforeAnySideEffect()) {
-      if (info != null) {
-        info.exclude(invoke, "receiver for candidate can be null");
-      }
-      return null;
-    }
-
-    Reason reason = computeInliningReason(candidate);
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
-      // Abort inlining attempt if the single target is not an inlining candidate.
-      if (info != null) {
-        info.exclude(invoke, "target is not identified for inlining");
-      }
-      return null;
-    }
-
-    if (!passesInliningConstraints(invoke, candidate, reason)) {
-      return null;
-    }
-
-    if (info != null) {
-      info.include(invoke.getType(), candidate);
-    }
-    return new InlineAction(candidate, invoke, reason);
-  }
-
-  public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
-    DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
-      return null;
-    }
-
-    Reason reason = computeInliningReason(candidate);
-    // Determine if this should be inlined no matter how big it is.
-    if (!candidate.isInliningCandidate(method, reason, inliner.appInfo)) {
-      // Abort inlining attempt if the single target is not an inlining candidate.
-      if (info != null) {
-        info.exclude(invoke, "target is not identified for inlining");
-      }
-      return null;
-    }
-
-    // Abort inlining attempt if we can not guarantee class for static target has been initialized.
-    if (!canInlineStaticInvoke(method, candidate)) {
-      if (info != null) {
-        info.exclude(invoke, "target is static but we cannot guarantee class has been initialized");
-      }
-      return null;
-    }
-
-    if (!passesInliningConstraints(invoke, candidate, reason)) {
-      return null;
-    }
-
-    if (info != null) {
-      info.include(invoke.getType(), candidate);
-    }
-    return new InlineAction(candidate, invoke, reason);
-  }
-
-  public InlineAction computeForInvokePolymorpic(
-      InvokePolymorphic invoke, DexType invocationContext) {
-    // TODO: No inlining of invoke polymorphic for now.
-    if (info != null) {
-      info.exclude(invoke, "inlining through invoke signature polymorpic is not supported");
-    }
-    return null;
-  }
+  InlineAction computeForInvokePolymorphic(
+      InvokePolymorphic invoke, DexType invocationContext);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
new file mode 100644
index 0000000..2b0c7e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import java.util.ListIterator;
+
+interface InliningStrategy {
+  boolean exceededAllowance();
+
+  void markInlined(IRCode inlinee);
+
+  void ensureMethodProcessed(
+      DexEncodedMethod target, IRCode inlinee) throws ApiLevelException;
+
+  ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
+      ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor);
+
+  DexType getReceiverTypeIfKnown(InvokeMethod invoke);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index e9781b4..9462354 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -7,11 +7,11 @@
 import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -22,6 +22,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.code.Add;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -46,6 +47,7 @@
 import com.android.tools.r8.ir.conversion.SourceCode;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
@@ -1002,10 +1004,11 @@
     }
 
     @Override
-    public IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+    public IRCode buildIR(DexEncodedMethod encodedMethod,
+        AppInfo appInfo, InternalOptions options, Origin origin)
         throws ApiLevelException {
       OutlineSourceCode source = new OutlineSourceCode(outline);
-      IRBuilder builder = new IRBuilder(encodedMethod, source, options);
+      IRBuilder builder = new IRBuilder(encodedMethod, appInfo, source, options);
       return builder.build();
     }
 
@@ -1056,7 +1059,7 @@
       DexString methodName = dexItemFactory.createString(OutlineOptions.METHOD_PREFIX + count);
       DexMethod method = outline.buildMethod(type, methodName);
       direct[count] = new DexEncodedMethod(method, methodAccess, DexAnnotationSet.empty(),
-          DexAnnotationSetRefList.empty(), new OutlineCode(outline));
+          ParameterAnnotationsList.empty(), new OutlineCode(outline));
       generatedOutlines.put(outline, method);
       count++;
     }
@@ -1085,7 +1088,30 @@
             direct,
             DexEncodedMethod.EMPTY_ARRAY, // Virtual methods.
             options.itemFactory.getSkipNameValidationForTesting());
+    if (options.isGeneratingClassFiles()) {
+      // Don't set class file version below 50.0 (JDK release 1.6).
+      clazz.setClassFileVersion(Math.max(50, getClassFileVersion()));
+    }
 
     return clazz;
   }
+
+  private int getClassFileVersion() {
+    assert options.isGeneratingClassFiles();
+    int classFileVersion = -1;
+    Set<DexType> seen = Sets.newIdentityHashSet();
+    for (List<DexEncodedMethod> methods : candidates.values()) {
+      for (DexEncodedMethod method : methods) {
+        DexType holder = method.method.holder;
+        if (seen.add(holder)) {
+          DexProgramClass programClass = appInfo.definitionFor(holder).asProgramClass();
+          assert programClass != null : "Attempt to outline from library class";
+          assert programClass.originatesFromClassResource()
+              : "Attempt to outline from non-classfile input to classfile output";
+          classFileVersion = Math.max(classFileVersion, programClass.getClassFileVersion());
+        }
+      }
+    }
+    return classFileVersion;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java
index 25b1d1b..1284671 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/SwitchMapCollector.java
@@ -93,7 +93,7 @@
     List<DexEncodedField> switchMapFields = Arrays.stream(clazz.staticFields())
         .filter(this::maybeIsSwitchMap).collect(Collectors.toList());
     if (!switchMapFields.isEmpty()) {
-      IRCode initializer = clazz.getClassInitializer().buildIR(options);
+      IRCode initializer = clazz.getClassInitializer().buildIR(appInfo, options, clazz.origin);
       switchMapFields.forEach(field -> extractSwitchMap(field, initializer));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
new file mode 100644
index 0000000..7d13b1a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -0,0 +1,385 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner;
+
+import com.android.tools.r8.ApiLevelException;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.google.common.collect.Streams;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public final class ClassInliner {
+  private final DexItemFactory factory;
+  private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
+
+  private static final Map<DexField, Integer> NO_MAPPING = new IdentityHashMap<>();
+
+  public interface InlinerAction {
+    void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods) throws ApiLevelException;
+  }
+
+  public ClassInliner(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  // Process method code and inline eligible class instantiations, in short:
+  //
+  // - collect all 'new-instance' instructions in the original code. Note that class
+  // inlining, if happens, mutates code and can add 'new-instance' instructions.
+  // Processing them as well is possible, but does not seem to have much value.
+  //
+  // - for each 'new-instance' we check if it is eligible for inlining, i.e:
+  //     -> the class of the new instance is 'eligible' (see computeClassEligible(...))
+  //     -> the instance is initialized with 'eligible' constructor (see
+  //        onlyInitializesFieldsWithNoOtherSideEffects flag in method's optimization
+  //        info); eligible constructor also defines a set of instance fields directly
+  //        initialized with parameter values, called field initialization mapping below
+  //     -> has only 'eligible' uses, i.e:
+  //          * it is a receiver of a field read if the field is present in the
+  //            field initialization mapping
+  //          * it is a receiver of virtual or interface call with single target being
+  //            a method only reading fields in the current field initialization mapping
+  //
+  // - inline eligible 'new-instance' instructions, i.e:
+  //     -> force inline methods called on the instance (which may introduce additional
+  //        instance field reads, but only for fields present in the current field
+  //        initialization mapping)
+  //     -> replace instance field reads with appropriate values passed to the constructor
+  //        according to field initialization mapping
+  //     -> remove constructor call
+  //     -> remove 'new-instance' instructions
+  //
+  // For example:
+  //
+  // Original code:
+  //   class C {
+  //     static class L {
+  //       final int x;
+  //       L(int x) {
+  //         this.x = x;
+  //       }
+  //       int getX() {
+  //         return x;
+  //       }
+  //     }
+  //     static int method1() {
+  //       return new L(1).x;
+  //     }
+  //     static int method2() {
+  //       return new L(1).getX();
+  //     }
+  //   }
+  //
+  // Code after class C is 'inlined':
+  //   class C {
+  //     static int method1() {
+  //       return 1;
+  //     }
+  //     static int method2() {
+  //       return 1;
+  //     }
+  //   }
+  //
+  public final void processMethodCode(
+      AppInfoWithSubtyping appInfo,
+      DexEncodedMethod method,
+      IRCode code,
+      Predicate<DexEncodedMethod> isProcessedConcurrently,
+      InlinerAction inliner) throws ApiLevelException {
+
+    // Collect all the new-instance instructions in the code before inlining.
+    List<NewInstance> newInstances = Streams.stream(code.instructionIterator())
+        .filter(Instruction::isNewInstance)
+        .map(Instruction::asNewInstance)
+        .collect(Collectors.toList());
+
+    nextNewInstance:
+    for (NewInstance newInstance : newInstances) {
+      Value eligibleInstance = newInstance.outValue();
+      if (eligibleInstance == null) {
+        continue;
+      }
+
+      DexType eligibleClass = newInstance.clazz;
+      if (!isClassEligible(appInfo, eligibleClass)) {
+        continue;
+      }
+
+      // No Phi users.
+      if (eligibleInstance.numberOfPhiUsers() > 0) {
+        continue;
+      }
+
+      Set<Instruction> uniqueUsers = eligibleInstance.uniqueUsers();
+
+      // Find an initializer invocation.
+      InvokeDirect eligibleInitCall = null;
+      Map<DexField, Integer> mappings = null;
+      for (Instruction user : uniqueUsers) {
+        if (!user.isInvokeDirect()) {
+          continue;
+        }
+
+        InvokeDirect candidate = user.asInvokeDirect();
+        DexMethod candidateInit = candidate.getInvokedMethod();
+        if (factory.isConstructor(candidateInit) &&
+            candidate.inValues().lastIndexOf(eligibleInstance) == 0) {
+
+          if (candidateInit.holder != eligibleClass) {
+            // Inlined constructor call? We won't get field initialization mapping in this
+            // case, but since we only support eligible classes extending java.lang.Object,
+            // it's safe to assume an empty mapping.
+            if (candidateInit.holder == factory.objectType) {
+              mappings = Collections.emptyMap();
+            }
+
+          } else {
+            // Is it a call to an *eligible* constructor?
+            mappings = getConstructorFieldMappings(appInfo, candidateInit, isProcessedConcurrently);
+          }
+
+          eligibleInitCall = candidate;
+          break;
+        }
+      }
+
+      if (mappings == null) {
+        continue;
+      }
+
+      // Check all regular users.
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
+
+      for (Instruction user : uniqueUsers) {
+        if (user == eligibleInitCall) {
+          continue /* next user */;
+        }
+
+        if (user.isInstanceGet()) {
+          InstanceGet instanceGet = user.asInstanceGet();
+          if (mappings.containsKey(instanceGet.getField())) {
+            continue /* next user */;
+          }
+
+          // Not replaceable field read.
+          continue nextNewInstance;
+        }
+
+        if (user.isInvokeVirtual() || user.isInvokeInterface()) {
+          InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+          if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
+            continue nextNewInstance; // Instance must only be passes as a receiver.
+          }
+
+          DexEncodedMethod singleTarget =
+              findSingleTarget(appInfo, invoke, eligibleClass);
+          if (singleTarget == null) {
+            continue nextNewInstance;
+          }
+          if (isProcessedConcurrently.test(singleTarget)) {
+            continue nextNewInstance;
+          }
+          if (method == singleTarget) {
+            continue nextNewInstance; // Don't inline itself.
+          }
+
+          if (!singleTarget.getOptimizationInfo()
+              .isReceiverOnlyUsedForReadingFields(mappings.keySet())) {
+            continue nextNewInstance; // Target must be trivial.
+          }
+
+          if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+            // We won't be able to inline it here.
+
+            // Note that there may be some false negatives here since the method may
+            // reference private fields of its class which are supposed to be replaced
+            // with arguments after inlining. We should try and improve it later.
+
+            // Using -allowaccessmodification mitigates this.
+            continue nextNewInstance;
+          }
+
+          methodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
+          continue /* next user */;
+        }
+
+        continue nextNewInstance; // Unsupported user.
+      }
+
+      // Force-inline of method invocation if any.
+      inlineAllCalls(inliner, methodCalls);
+      assert assertOnlyConstructorAndFieldReads(eligibleInstance, eligibleInitCall, mappings);
+
+      // Replace all field reads with arguments passed to the constructor.
+      patchFieldReads(eligibleInstance, eligibleInitCall, mappings);
+      assert assertOnlyConstructor(eligibleInstance, eligibleInitCall);
+
+      // Remove constructor call and new-instance instructions.
+      removeInstruction(eligibleInitCall);
+      removeInstruction(newInstance);
+      code.removeAllTrivialPhis();
+    }
+  }
+
+  private void inlineAllCalls(InlinerAction inliner,
+      Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) throws ApiLevelException {
+    if (!methodCalls.isEmpty()) {
+      inliner.inline(methodCalls);
+    }
+  }
+
+  private void patchFieldReads(
+      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (!user.isInstanceGet()) {
+        continue;
+      }
+      InstanceGet fieldRead = user.asInstanceGet();
+
+      // Replace the field read with
+      assert mappings.containsKey(fieldRead.getField());
+      Value arg = invokeMethod.inValues().get(1 + mappings.get(fieldRead.getField()));
+      assert arg != null;
+      Value value = fieldRead.outValue();
+      if (value != null) {
+        value.replaceUsers(arg);
+        assert value.numberOfAllUsers() == 0;
+      }
+
+      // Remove instruction.
+      removeInstruction(fieldRead);
+    }
+  }
+
+  private void removeInstruction(Instruction instruction) {
+    instruction.inValues().forEach(v -> v.removeUser(instruction));
+    instruction.getBlock().removeInstruction(instruction);
+  }
+
+  private boolean assertOnlyConstructorAndFieldReads(
+      Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (user != invokeMethod &&
+          !(user.isInstanceGet() && mappings.containsKey(user.asFieldInstruction().getField()))) {
+        throw new Unreachable("Not all calls are inlined!");
+      }
+    }
+    return true;
+  }
+
+  private boolean assertOnlyConstructor(Value instance, InvokeDirect invokeMethod) {
+    for (Instruction user : instance.uniqueUsers()) {
+      if (user != invokeMethod) {
+        throw new Unreachable("Not all field reads are substituted!");
+      }
+    }
+    return true;
+  }
+
+  private DexEncodedMethod findSingleTarget(
+      AppInfo appInfo, InvokeMethodWithReceiver invoke, DexType instanceType) {
+
+    // We don't use computeSingleTarget(...) on invoke since it sometimes fails to
+    // find the single target, while this code may be more successful since we exactly
+    // know what is the actual type of the receiver.
+
+    // Note that we also intentionally limit ourselves to methods directly defined in
+    // the instance's class. This may be improved later.
+
+    DexClass clazz = appInfo.definitionFor(instanceType);
+    if (clazz != null) {
+      DexMethod callee = invoke.getInvokedMethod();
+      for (DexEncodedMethod candidate : clazz.virtualMethods()) {
+        if (candidate.method.name == callee.name && candidate.method.proto == callee.proto) {
+          return candidate;
+        }
+      }
+    }
+    return null;
+  }
+
+  private Map<DexField, Integer> getConstructorFieldMappings(
+      AppInfo appInfo, DexMethod init, Predicate<DexEncodedMethod> isProcessedConcurrently) {
+    assert isClassEligible(appInfo, init.holder);
+
+    DexEncodedMethod definition = appInfo.definitionFor(init);
+    if (definition == null) {
+      return NO_MAPPING;
+    }
+
+    if (isProcessedConcurrently.test(definition)) {
+      return NO_MAPPING;
+    }
+
+    if (definition.accessFlags.isAbstract() || definition.accessFlags.isNative()) {
+      return NO_MAPPING;
+    }
+
+    return definition.getOptimizationInfo().onlyInitializesFieldsWithNoOtherSideEffects();
+  }
+
+  private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
+    Boolean eligible = knownClasses.get(clazz);
+    if (eligible == null) {
+      Boolean computed = computeClassEligible(appInfo, clazz);
+      Boolean existing = knownClasses.putIfAbsent(clazz, computed);
+      assert existing == null || existing == computed;
+      eligible = existing == null ? computed : existing;
+    }
+    return eligible;
+  }
+
+  // Class is eligible for this optimization. Eligibility implementation:
+  //   - not an abstract or interface
+  //   - directly extends java.lang.Object
+  //   - does not declare finalizer
+  //   - does not trigger any static initializers
+  private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
+    DexClass definition = appInfo.definitionFor(clazz);
+    if (definition == null || definition.isLibraryClass() ||
+        definition.accessFlags.isAbstract() || definition.accessFlags.isInterface()) {
+      return false;
+    }
+
+    // Must directly extend Object.
+    if (definition.superType != factory.objectType) {
+      return false;
+    }
+
+    // Class must not define finalizer.
+    for (DexEncodedMethod method : definition.virtualMethods()) {
+      if (method.method.name == factory.finalizeMethodName &&
+          method.method.proto == factory.objectMethods.finalize.proto) {
+        return false;
+      }
+    }
+
+    // Check for static initializers in this class or any of interfaces it implements.
+    return !appInfo.canTriggerStaticInitializer(clazz);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
index 03daf44..466117f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -327,12 +327,12 @@
 
     for (DexEncodedMethod method : clazz.directMethods()) {
       lambdaInvalidator.accept(method.annotations);
-      lambdaInvalidator.accept(method.parameterAnnotations);
+      lambdaInvalidator.accept(method.parameterAnnotationsList);
       lambdaInvalidator.accept(method.method, clazz.type);
     }
     for (DexEncodedMethod method : clazz.virtualMethods()) {
       lambdaInvalidator.accept(method.annotations);
-      lambdaInvalidator.accept(method.parameterAnnotations);
+      lambdaInvalidator.accept(method.parameterAnnotationsList);
       lambdaInvalidator.accept(method.method, clazz.type);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java
index 4f56306..05c71e6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaTypeVisitor.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexField;
@@ -24,6 +23,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueMethodHandle;
 import com.android.tools.r8.graph.DexValue.DexValueMethodType;
 import com.android.tools.r8.graph.DexValue.DexValueType;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -116,10 +116,8 @@
     }
   }
 
-  void accept(DexAnnotationSetRefList annotationSetRefList) {
-    for (DexAnnotationSet annotationSet : annotationSetRefList.values) {
-      accept(annotationSet);
-    }
+  void accept(ParameterAnnotationsList parameterAnnotationsList) {
+    parameterAnnotationsList.forEachAnnotation(this::accept);
   }
 
   private void accept(DexAnnotation annotation) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupClassBuilder.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupClassBuilder.java
index d441a00..1fe0317 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupClassBuilder.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -19,6 +18,7 @@
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.optimize.lambda.LambdaGroupClassBuilder;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.ir.synthetic.SyntheticSourceCode;
@@ -113,7 +113,7 @@
             factory.createMethod(group.getGroupClassType(), methodProto, methodName),
             accessFlags,
             isMainMethod ? id.mainMethodAnnotations : DexAnnotationSet.empty(),
-            isMainMethod ? id.mainMethodParamAnnotations : DexAnnotationSetRefList.empty(),
+            isMainMethod ? id.mainMethodParamAnnotations : ParameterAnnotationsList.empty(),
             new SynthesizedCode(
                 new KotlinLambdaVirtualMethodSourceCode(factory, group.getGroupClassType(),
                     methodProto, group.getLambdaIdField(factory), implMethods))));
@@ -159,7 +159,7 @@
         factory.createMethod(groupClassType, initializerProto, factory.constructorMethodName),
         CONSTRUCTOR_FLAGS_RELAXED,  // always create access-relaxed constructor.
         DexAnnotationSet.empty(),
-        DexAnnotationSetRefList.empty(),
+        ParameterAnnotationsList.empty(),
         new SynthesizedCode(createInstanceInitializerSourceCode(groupClassType, initializerProto)));
 
     // Static class initializer for stateless lambdas.
@@ -170,7 +170,7 @@
               factory.classConstructorMethodName),
           CLASS_INITIALIZER_FLAGS,
           DexAnnotationSet.empty(),
-          DexAnnotationSetRefList.empty(),
+          ParameterAnnotationsList.empty(),
           new SynthesizedCode(new ClassInitializerSourceCode(factory, group)));
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupId.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupId.java
index 0540bef..996099b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupId.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupId.java
@@ -5,13 +5,13 @@
 package com.android.tools.r8.ir.optimize.lambda.kotlin;
 
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
 import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
 import com.android.tools.r8.ir.optimize.lambda.LambdaGroupId;
 
@@ -41,7 +41,7 @@
   final DexString mainMethodName;
   final DexProto mainMethodProto;
   final DexAnnotationSet mainMethodAnnotations;
-  final DexAnnotationSetRefList mainMethodParamAnnotations;
+  final ParameterAnnotationsList mainMethodParamAnnotations;
 
   final EnclosingMethodAttribute enclosing;
 
@@ -61,7 +61,7 @@
     this.mainMethodName = mainMethod.method.name;
     this.mainMethodProto = mainMethod.method.proto;
     this.mainMethodAnnotations = mainMethod.annotations;
-    this.mainMethodParamAnnotations = mainMethod.parameterAnnotations;
+    this.mainMethodParamAnnotations = mainMethod.parameterAnnotationsList;
     this.innerClassAccess = inner != null ? inner.getAccess() : MISSING_INNER_CLASS_ATTRIBUTE;
     this.enclosing = enclosing;
     this.hash = computeHashCode();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
index f5698aa..42e0bb1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/kotlin/KotlinLambdaGroupIdFactory.java
@@ -191,7 +191,7 @@
       throw new LambdaStructureError("unexpected method annotations [" +
           method.annotations.toSmaliString() + "] on " + method.method.toSourceString());
     }
-    if (!method.parameterAnnotations.isEmpty()) {
+    if (!method.parameterAnnotationsList.isEmpty()) {
       throw new LambdaStructureError("unexpected method parameters annotations [" +
           method.annotations.toSmaliString() + "] on " + method.method.toSourceString());
     }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
index 53bc57e..f8134f4 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SynthesizedCode.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.ApiLevelException;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.UseRegistry;
@@ -13,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.SourceCode;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
 import java.util.function.Consumer;
 
@@ -37,9 +39,11 @@
   }
 
   @Override
-  public final IRCode buildIR(DexEncodedMethod encodedMethod, InternalOptions options)
+  public final IRCode buildIR(
+      DexEncodedMethod encodedMethod, AppInfo appInfo,
+      InternalOptions options, Origin origin)
       throws ApiLevelException {
-    return new IRBuilder(encodedMethod, sourceCode, options).build();
+    return new IRBuilder(encodedMethod, appInfo, sourceCode, options).build();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index e49ad85..2970fb4 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -34,6 +33,7 @@
 import com.android.tools.r8.graph.DexValue.UnknownDexValue;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.JarClassFileReader;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.utils.ExceptionUtils;
@@ -252,7 +252,7 @@
       }
     }
     writeAnnotations(visitor::visitAnnotation, method.annotations.annotations);
-    writeParameterAnnotations(visitor, method.parameterAnnotations);
+    writeParameterAnnotations(visitor, method.parameterAnnotationsList);
     if (!method.accessFlags.isAbstract() && !method.accessFlags.isNative()) {
       writeCode(method.getCode(), visitor);
     }
@@ -260,21 +260,21 @@
   }
 
   private void writeParameterAnnotations(
-      MethodVisitor visitor, DexAnnotationSetRefList parameterAnnotations) {
-    int missingParameterAnnotations = parameterAnnotations.getMissingParameterAnnotations();
-    for (int i = 0; i < missingParameterAnnotations; i++) {
-      AnnotationVisitor av =
-          visitor.visitParameterAnnotation(i, JarClassFileReader.SYNTHETIC_ANNOTATION, false);
-      if (av != null) {
-        av.visitEnd();
+      MethodVisitor visitor, ParameterAnnotationsList parameterAnnotations) {
+    for (int i = 0; i < parameterAnnotations.size(); i++) {
+      if (parameterAnnotations.isMissing(i)) {
+        AnnotationVisitor av =
+            visitor.visitParameterAnnotation(i, JarClassFileReader.SYNTHETIC_ANNOTATION, false);
+        if (av != null) {
+          av.visitEnd();
+        }
+      } else {
+        int iFinal = i;
+        writeAnnotations(
+            (d, vis) -> visitor.visitParameterAnnotation(iFinal, d, vis),
+            parameterAnnotations.get(i).annotations);
       }
     }
-    for (int i = 0; i < parameterAnnotations.values.length; i++) {
-      int parameterIndex = i + missingParameterAnnotations;
-      writeAnnotations(
-          (d, vis) -> visitor.visitParameterAnnotation(parameterIndex, d, vis),
-          parameterAnnotations.values[i].annotations);
-    }
   }
 
   private interface AnnotationConsumer {
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 871812a..28ca9fc 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -5,8 +5,6 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexAnnotation;
-import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -15,9 +13,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.function.Predicate;
 
 public class AnnotationRemover {
 
@@ -127,20 +122,20 @@
     keep.ensureValid(options.forceProguardCompatibility, compatibility);
     for (DexProgramClass clazz : appInfo.classes()) {
       stripAttributes(clazz);
-      clazz.annotations = stripAnnotations(clazz.annotations, this::filterAnnotations);
+      clazz.annotations = clazz.annotations.keepIf(this::filterAnnotations);
       clazz.forEachMethod(this::processMethod);
       clazz.forEachField(this::processField);
     }
   }
 
   private void processMethod(DexEncodedMethod method) {
-    method.annotations = stripAnnotations(method.annotations, this::filterAnnotations);
-    method.parameterAnnotations = stripAnnotations(method.parameterAnnotations,
-        this::filterParameterAnnotations);
+    method.annotations = method.annotations.keepIf(this::filterAnnotations);
+    method.parameterAnnotationsList =
+        method.parameterAnnotationsList.keepIf(this::filterParameterAnnotations);
   }
 
   private void processField(DexEncodedField field) {
-      field.annotations = stripAnnotations(field.annotations, this::filterAnnotations);
+    field.annotations = field.annotations.keepIf(this::filterAnnotations);
   }
 
   private void stripAttributes(DexProgramClass clazz) {
@@ -152,52 +147,4 @@
     }
   }
 
-  private DexAnnotationSetRefList stripAnnotations(DexAnnotationSetRefList annotations,
-      Predicate<DexAnnotation> filter) {
-    DexAnnotationSet[] filtered = null;
-    for (int i = 0; i < annotations.values.length; i++) {
-      DexAnnotationSet updated = stripAnnotations(annotations.values[i], filter);
-      if (updated != annotations.values[i]) {
-        if (filtered == null) {
-          filtered = annotations.values.clone();
-          filtered[i] = updated;
-        }
-      }
-    }
-    if (filtered == null) {
-      return annotations;
-    } else {
-      if (Arrays.stream(filtered).allMatch(DexAnnotationSet::isEmpty)) {
-        return DexAnnotationSetRefList.empty();
-      }
-      return new DexAnnotationSetRefList(filtered);
-    }
-  }
-
-  private DexAnnotationSet stripAnnotations(DexAnnotationSet annotations,
-      Predicate<DexAnnotation> filter) {
-    ArrayList<DexAnnotation> filtered = null;
-    for (int i = 0; i < annotations.annotations.length; i++) {
-      DexAnnotation annotation = annotations.annotations[i];
-      if (filter.test(annotation)) {
-        if (filtered != null) {
-          filtered.add(annotation);
-        }
-      } else {
-        if (filtered == null) {
-          filtered = new ArrayList<>(annotations.annotations.length);
-          for (int j = 0; j < i; j++) {
-            filtered.add(annotations.annotations[j]);
-          }
-        }
-      }
-    }
-    if (filtered == null) {
-      return annotations;
-    } else if (filtered.isEmpty()) {
-      return DexAnnotationSet.empty();
-    } else {
-      return new DexAnnotationSet(filtered.toArray(new DexAnnotation[filtered.size()]));
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index c0af336..a87de4b 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.Descriptor;
 import com.android.tools.r8.graph.DexAnnotation;
-import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
@@ -552,14 +551,18 @@
 
   private void processAnnotations(DexAnnotation[] annotations) {
     for (DexAnnotation annotation : annotations) {
-      DexType type = annotation.annotation.type;
-      if (liveTypes.contains(type)) {
-        // The type of this annotation is already live, so pick up its dependencies.
-        handleAnnotationOfLiveType(annotation);
-      } else {
-        // Remember this annotation for later.
-        deferredAnnotations.computeIfAbsent(type, ignore -> new HashSet<>()).add(annotation);
-      }
+      processAnnotation(annotation);
+    }
+  }
+
+  private void processAnnotation(DexAnnotation annotation) {
+    DexType type = annotation.annotation.type;
+    if (liveTypes.contains(type)) {
+      // The type of this annotation is already live, so pick up its dependencies.
+      handleAnnotationOfLiveType(annotation);
+    } else {
+      // Remember this annotation for later.
+      deferredAnnotations.computeIfAbsent(type, ignore -> new HashSet<>()).add(annotation);
     }
   }
 
@@ -1239,9 +1242,7 @@
         }
       }
       processAnnotations(method.annotations.annotations);
-      for (DexAnnotationSet parameterAnnotation : method.parameterAnnotations.values) {
-        processAnnotations(parameterAnnotation.annotations);
-      }
+      method.parameterAnnotationsList.forEachAnnotation(this::processAnnotation);
       if (protoLiteExtension != null && protoLiteExtension.appliesTo(method)) {
         protoLiteExtension.processMethod(method, new UseRegistry(method), protoLiteFields);
       } else {
@@ -1343,7 +1344,7 @@
 
   private void handleProguardReflectiveBehavior(DexEncodedMethod method) {
     try {
-      IRCode code = method.buildIR(options);
+      IRCode code = method.buildIR(appInfo, options, appInfo.originFor(method.method.holder));
       code.instructionIterator().forEachRemaining(this::handleProguardReflectiveBehavior);
     } catch (ApiLevelException e) {
       // Ignore this exception here. It will be hit again further in the pipeline when
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5f5237b..8eb8e9c 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -65,6 +65,8 @@
       "dontskipnonpubliclibraryclasses",
       "dontskipnonpubliclibraryclassmembers",
       "invokebasemethod",
+      // TODO(b/62524562): we may support this later.
+      "mergeinterfacesaggressively",
       "android");
 
   private static final List<String> IGNORED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList.of(
@@ -72,8 +74,7 @@
       "whyarenotsimple");
 
   private static final List<String> WARNED_SINGLE_ARG_OPTIONS = ImmutableList.of(
-      // TODO -outjars (http://b/37137994) and -adaptresourcefilecontents (http://b/37139570)
-      // should be reported as errors, not just as warnings!
+      // TODO(b/37137994): -outjars should be reported as errors, not just as warnings!
       "outjars");
 
   private static final List<String> WARNED_FLAG_OPTIONS = ImmutableList.of(
@@ -137,8 +138,7 @@
     parse(ImmutableList.of(new ProguardConfigurationSourceFile(path)));
   }
 
-  // package visible for testing
-  void parse(ProguardConfigurationSource source) {
+  public void parse(ProguardConfigurationSource source) {
     parse(ImmutableList.of(source));
   }
 
@@ -339,9 +339,11 @@
       } else if (acceptString("adaptclassstrings")) {
         parseClassFilter(configurationBuilder::addAdaptClassStringsPattern);
       } else if (acceptString("adaptresourcefilenames")) {
+        // TODO(b/76377381): should be report an error until it's fully supported.
         parsePathFilter(configurationBuilder::addAdaptResourceFilenames);
       } else if (acceptString("adaptresourcefilecontents")) {
         parsePathFilter(configurationBuilder::addAdaptResourceFilecontents);
+        // TODO(b/37139570): should be report an error until it's fully supported.
       } else if (acceptString("identifiernamestring")) {
         configurationBuilder.addRule(parseIdentifierNameStringRule());
       } else if (acceptString("if")) {
@@ -968,6 +970,11 @@
     private Path parseFileName() throws ProguardRuleParserException {
       TextPosition start = getPosition();
       skipWhitespace();
+
+      if (baseDirectory == null) {
+        throw parseError("Options with file names are not supported", start);
+      }
+
       String fileName = acceptString(character ->
           character != File.pathSeparatorChar
               && !Character.isWhitespace(character)
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
new file mode 100644
index 0000000..3f379a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+
+public class ProguardConfigurationSourceBytes implements ProguardConfigurationSource {
+  private final byte[] bytes;
+  private final Origin origin;
+
+  public ProguardConfigurationSourceBytes(byte[] bytes, Origin origin) {
+    this.bytes = bytes;
+    this.origin = origin;
+  }
+
+  public ProguardConfigurationSourceBytes(InputStream in, Origin origin) throws IOException {
+    this(ByteStreams.toByteArray(in), origin);
+  }
+
+  @Override
+  public String get() throws IOException {
+    return new String(bytes, StandardCharsets.UTF_8);
+  }
+
+  @Override
+  public Path getBaseDirectory() {
+    // Options with relative names (including -include) is not supported.
+    return null;
+  }
+
+  @Override
+  public String getName() {
+    return origin.toString();
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+}
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 d8dffea..1b026c0 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -589,5 +589,9 @@
         throw new CompilationError("Unsupported source file type", new PathOrigin(file));
       }
     }
+
+    public List<ProgramResourceProvider> getProgramResourceProviders() {
+      return programResourceProviders;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
index 9fd91d9..99149ff 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import java.io.FileNotFoundException;
@@ -19,6 +20,10 @@
     this.origin = origin;
   }
 
+  public ExceptionDiagnostic(ResourceException e) {
+    this(e, e.getOrigin());
+  }
+
   @Override
   public Origin getOrigin() {
     return origin;
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 3086662..92c68aa 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -4,10 +4,10 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.ClassFileConsumer;
-import com.android.tools.r8.DexFilePerClassFileConsumer;
-import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DataResourceConsumer;
 import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DexFilePerClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.dex.Marker;
@@ -81,6 +81,7 @@
       enableDevirtualization = false;
       enableNonNullTracking = false;
       enableInlining = false;
+      enableClassInlining = false;
       enableSwitchMapRemoval = false;
       outline.enabled = false;
       enableValuePropagation = false;
@@ -97,6 +98,7 @@
   public boolean enableDevirtualization = true;
   public boolean enableNonNullTracking = true;
   public boolean enableInlining = true;
+  public boolean enableClassInlining = true;
   public int inliningInstructionLimit = 5;
   public boolean enableSwitchMapRemoval = true;
   public final OutlineOptions outline = new OutlineOptions();
@@ -183,6 +185,9 @@
   public OffOrAuto interfaceMethodDesugaring = OffOrAuto.Auto;
   // Defines try-with-resources rewriter behavior.
   public OffOrAuto tryWithResourcesDesugaring = OffOrAuto.Auto;
+  // Flag to turn on/off processing of @dalvik.annotation.codegen.CovariantReturnType and
+  // @dalvik.annotation.codegen.CovariantReturnType$CovariantReturnTypes.
+  public boolean processCovariantReturnTypeAnnotations = true;
 
   // Whether or not to check for valid multi-dex builds.
   //
diff --git a/src/main/java/com/android/tools/r8/utils/Reporter.java b/src/main/java/com/android/tools/r8/utils/Reporter.java
index 63b95b3..ebb7ce6 100644
--- a/src/main/java/com/android/tools/r8/utils/Reporter.java
+++ b/src/main/java/com/android/tools/r8/utils/Reporter.java
@@ -8,6 +8,8 @@
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -80,7 +82,15 @@
       if (errorCount != 0) {
         AbortException abort;
         if (lastError != null && lastError.getDiagnosticMessage() != null) {
-          abort = new AbortException("Error: " + lastError.getDiagnosticMessage());
+          StringBuilder builder = new StringBuilder("Error: ");
+          if (lastError.getOrigin() != Origin.unknown()) {
+            builder.append(lastError.getOrigin()).append(", ");
+          }
+          if (lastError.getPosition() != Position.UNKNOWN) {
+            builder.append(lastError.getPosition()).append(", ");
+          }
+          builder.append(lastError.getDiagnosticMessage());
+          abort = new AbortException(builder.toString());
         } else {
           abort = new AbortException();
         }
diff --git a/src/test/debugTestResourcesKotlin/KotlinInline.kt b/src/test/debugTestResourcesKotlin/KotlinInline.kt
index 7f914e4..4b55c57 100644
--- a/src/test/debugTestResourcesKotlin/KotlinInline.kt
+++ b/src/test/debugTestResourcesKotlin/KotlinInline.kt
@@ -47,6 +47,30 @@
         emptyMethod(-1)
     }
 
+    // Double inlining
+    fun testNestedInlining() {
+        val l1 = Int.MAX_VALUE
+        val l2 = Int.MIN_VALUE
+        inlinee1(l1, l2)
+    }
+    inline fun inlinee1(a: Int, b: Int) {
+        val c = a - 2
+        inlinee2(1) {
+            val left = a + b
+            val right = a - b
+            foo(left, right)
+        }
+        inlinee2(c) {
+            foo(b, a)
+        }
+    }
+
+    inline fun inlinee2(p: Int, block: () -> Unit) {
+        if (p > 0) {
+            block()
+        }
+    }
+
     companion object {
         @JvmStatic fun main(args: Array<String>) {
             println("Hello world!")
@@ -54,6 +78,7 @@
             instance.processObject(instance, instance::printObject)
             instance.invokeInlinedFunctions()
             instance.singleInline()
+            instance.testNestedInlining()
         }
     }
 }
\ No newline at end of file
diff --git a/src/test/examplesAndroidO/lambdadesugaring/ValueAdjustments.java b/src/test/examplesAndroidO/lambdadesugaring/ValueAdjustments.java
index f2344b5..1d6772a 100644
--- a/src/test/examplesAndroidO/lambdadesugaring/ValueAdjustments.java
+++ b/src/test/examplesAndroidO/lambdadesugaring/ValueAdjustments.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package lambdadesugaring;
 
+import java.io.Serializable;
+
 public class ValueAdjustments {
   interface B2i {
     int foo(Byte i);
@@ -37,6 +39,22 @@
     Number f();
   }
 
+  interface iSerializableOut {
+    Serializable f();
+  }
+
+  interface iSerializableInt {
+    Object f(Serializable s);
+  }
+
+  interface iComparableOut<T> {
+    Comparable<T> f();
+  }
+
+  interface iComparableInt<T> {
+    Object f(Comparable<T> c);
+  }
+
   interface iByte {
     Byte f();
   }
@@ -164,6 +182,30 @@
         .append(((iNumber) ValueAdjustments::D).f()).append('\n');
   }
 
+  private static void checkSerializableOut(StringBuffer builder) {
+    builder
+        .append(((iSerializableOut) ValueAdjustments::z).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::c).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::b).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::s).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::i).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::j).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::f).f()).append(' ')
+        .append(((iSerializableOut) ValueAdjustments::d).f()).append(' ');
+  }
+
+  private static void checkComparableOut(StringBuffer builder) {
+    builder
+        .append(((iComparableOut) ValueAdjustments::z).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::c).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::b).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::s).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::i).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::j).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::f).f()).append(' ')
+        .append(((iComparableOut) ValueAdjustments::d).f()).append(' ');
+  }
+
   private static void checkBoxes(StringBuffer builder) {
     builder
         .append(((iBoolean) ValueAdjustments::z).f()).append(' ')
@@ -310,20 +352,44 @@
         .append(((BnUnB) ValueAdjustments::boxingAndUnboxingW).foo(true, false, (byte) 1, (byte) 2,
             (char) 33, (char) 44, (short) 5, (short) 6, 7, 8, 9, 10L, 11, 12f, 13, 14d))
         .append('\n')
+        .append(((BnUnB) ValueAdjustments::allSerializable).foo(true, false, (byte) 1, (byte) 2,
+            (char) 33, (char) 44, (short) 5, (short) 6, 7, 8, 9, 10L, 11, 12f, 13, 14d))
+        .append('\n')
+        .append(((BnUnB) ValueAdjustments::allComparable).foo(true, false, (byte) 1, (byte) 2,
+            (char) 33, (char) 44, (short) 5, (short) 6, 7, 8, 9, 10L, 11, 12f, 13, 14d))
+        .append('\n')
         .append(((B2i) (Integer::new)).foo(Byte.valueOf((byte) 44))).append('\n');
   }
 
   static String boxingAndUnboxing(Boolean Z, boolean z, Byte B, byte b, Character C, char c,
       Short S, short s, Integer I, int i, Long L, long l, Float F, float f, Double D, double d) {
-    return "" + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S + ":" + s
-        + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
+    return "boxingAndUnboxing: " + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S +
+        ":" + s + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
   }
 
   static String boxingAndUnboxingW(boolean Z, boolean z, double B, double b,
       double C, double c, double S, double s, double I, double i, double L, double l,
       double F, double f, double D, double d) {
-    return "" + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S + ":" + s
-        + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
+    return "boxingAndUnboxingW: " + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S +
+        ":" + s + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
+  }
+
+  static String allSerializable(
+      Serializable Z, Serializable z, Serializable B, Serializable b,
+      Serializable C, Serializable c, Serializable S, Serializable s,
+      Serializable I, Serializable i, Serializable L, Serializable l,
+      Serializable F, Serializable f, Serializable D, Serializable d) {
+    return "allSerializable: " + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S +
+        ":" + s + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
+  }
+
+  static String allComparable(
+      Comparable Z, Comparable z, Comparable B, Comparable b,
+      Comparable C, Comparable c, Comparable S, Comparable s,
+      Comparable I, Comparable i, Comparable L, Comparable l,
+      Comparable F, Comparable f, Comparable D, Comparable d) {
+    return "allComparable: " + Z + ":" + z + ":" + B + ":" + b + ":" + C + ":" + c + ":" + S +
+        ":" + s + ":" + I + ":" + i + ":" + L + ":" + l + ":" + F + ":" + f + ":" + D + ":" + d;
   }
 
   static boolean z() {
@@ -393,7 +459,7 @@
   private static void bB70348575(StringBuffer builder) {
     B70348575_C1 c1 = new B70348575_C1();
     B70348575_A1 a = c1.getB().get();
-    builder.append(a.greet()).append('\n');;
+    builder.append(a.greet()).append('\n');
   }
 
   public static void main(String[] args) {
@@ -410,6 +476,8 @@
 
     checkBoxes(builder);
     checkNumber(builder);
+    checkSerializableOut(builder);
+    checkComparableOut(builder);
     checkObject(builder);
 
     checkMisc(builder);
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index 18c3436..4e07849 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -13,110 +13,287 @@
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.BiConsumer;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.util.ASMifier;
 import org.objectweb.asm.util.TraceClassVisitor;
 
-@RunWith(Parameterized.class)
 public class CfFrontendExamplesTest extends TestBase {
 
-  static final Collection<Object[]> TESTS = Arrays.asList(
-    makeTest("arithmetic.Arithmetic"),
-    makeTest("arrayaccess.ArrayAccess"),
-    makeTest("barray.BArray"),
-    makeTest("bridge.BridgeMethod"),
-    makeTest("cse.CommonSubexpressionElimination"),
-    makeTest("constants.Constants"),
-    makeTest("controlflow.ControlFlow"),
-    makeTest("conversions.Conversions"),
-    makeTest("floating_point_annotations.FloatingPointValuedAnnotationTest"),
-    makeTest("filledarray.FilledArray"),
-    makeTest("hello.Hello"),
-    makeTest("ifstatements.IfStatements"),
-    makeTest("instancevariable.InstanceVariable"),
-    makeTest("instanceofstring.InstanceofString"),
-    makeTest("invoke.Invoke"),
-    makeTest("jumbostring.JumboString"),
-    makeTest("loadconst.LoadConst"),
-    makeTest("loop.UdpServer"),
-    makeTest("newarray.NewArray"),
-    makeTest("regalloc.RegAlloc"),
-    makeTest("returns.Returns"),
-    makeTest("staticfield.StaticField"),
-    makeTest("stringbuilding.StringBuilding"),
-    makeTest("switches.Switches"),
-    makeTest("sync.Sync"),
-    makeTest("throwing.Throwing"),
-    makeTest("trivial.Trivial"),
-    makeTest("trycatch.TryCatch"),
-    makeTest("nestedtrycatches.NestedTryCatches"),
-    makeTest("trycatchmany.TryCatchMany"),
-    makeTest("invokeempty.InvokeEmpty"),
-    makeTest("regress.Regress"),
-    makeTest("regress2.Regress2"),
-    makeTest("regress_37726195.Regress"),
-    makeTest("regress_37658666.Regress", CfFrontendExamplesTest::compareRegress37658666),
-    makeTest("regress_37875803.Regress"),
-    makeTest("regress_37955340.Regress"),
-    makeTest("regress_62300145.Regress"),
-    makeTest("regress_64881691.Regress"),
-    makeTest("regress_65104300.Regress"),
-    makeTest("regress_70703087.Test"),
-    makeTest("regress_70736958.Test"),
-    makeTest("regress_70737019.Test"),
-    makeTest("regress_72361252.Test"),
-    makeTest("memberrebinding2.Memberrebinding"),
-    makeTest("memberrebinding3.Memberrebinding"),
-    makeTest("minification.Minification"),
-    makeTest("enclosingmethod.Main"),
-    makeTest("enclosingmethod_proguarded.Main"),
-    makeTest("interfaceinlining.Main"),
-    makeTest("switchmaps.Switches")
-  );
-
-  private static Object[] makeTest(String className) {
-    return makeTest(className, null);
-  }
-
-  private static Object[] makeTest(String className, BiConsumer<byte[], byte[]> comparator) {
-    return new Object[] {className, comparator};
-  }
-
-  @Parameters(name = "{0}")
-  public static Collection<Object[]> data() {
-    return TESTS;
-  }
-
-  private static void compareRegress37658666(byte[] expectedBytes, byte[] actualBytes) {
-    // javac emits LDC(-0.0f) instead of the shorter FCONST_0 FNEG emitted by CfConstNumber.
-    String ldc = "mv.visitLdcInsn(new Float(\"-0.0\"));";
-    String constNeg = "mv.visitInsn(FCONST_0);\nmv.visitInsn(FNEG);";
-    assertEquals(
-        asmToString(expectedBytes).replace(ldc, constNeg),
-        asmToString(actualBytes));
-  }
-
-  private final Path inputJar;
-  private final BiConsumer<byte[], byte[]> comparator;
-
-  public CfFrontendExamplesTest(String clazz, BiConsumer<byte[], byte[]> comparator) {
-    this.comparator = comparator;
-    String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
-    String suffix = "_debuginfo_all";
-    inputJar = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, pkg + suffix + JAR_EXTENSION);
+  @Test
+  public void testArithmetic() throws Exception {
+    runTest("arithmetic.Arithmetic");
   }
 
   @Test
-  public void test() throws Exception {
+  public void testArrayAccess() throws Exception {
+    runTest("arrayaccess.ArrayAccess");
+  }
+
+  @Test
+  public void testBArray() throws Exception {
+    runTest("barray.BArray");
+  }
+
+  @Test
+  public void testBridgeMethod() throws Exception {
+    runTest("bridge.BridgeMethod");
+  }
+
+  @Test
+  public void testCommonSubexpressionElimination() throws Exception {
+    runTest("cse.CommonSubexpressionElimination");
+  }
+
+  @Test
+  public void testConstants() throws Exception {
+    runTest("constants.Constants");
+  }
+
+  @Test
+  public void testControlFlow() throws Exception {
+    runTest("controlflow.ControlFlow");
+  }
+
+  @Test
+  public void testConversions() throws Exception {
+    runTest("conversions.Conversions");
+  }
+
+  @Test
+  public void testFloatingPointValuedAnnotation() throws Exception {
+    runTest("floating_point_annotations.FloatingPointValuedAnnotationTest");
+  }
+
+  @Test
+  public void testFilledArray() throws Exception {
+    runTest("filledarray.FilledArray");
+  }
+
+  @Test
+  public void testHello() throws Exception {
+    runTest("hello.Hello");
+  }
+
+  @Test
+  public void testIfStatements() throws Exception {
+    runTest("ifstatements.IfStatements");
+  }
+
+  @Test
+  public void testInstanceVariable() throws Exception {
+    runTest("instancevariable.InstanceVariable");
+  }
+
+  @Test
+  public void testInstanceofString() throws Exception {
+    runTest("instanceofstring.InstanceofString");
+  }
+
+  @Test
+  public void testInvoke() throws Exception {
+    runTest("invoke.Invoke");
+  }
+
+  @Test
+  public void testJumboString() throws Exception {
+    runTest("jumbostring.JumboString");
+  }
+
+  @Test
+  public void testLoadConst() throws Exception {
+    runTest("loadconst.LoadConst");
+  }
+
+  @Test
+  public void testUdpServer() throws Exception {
+    runTest("loop.UdpServer");
+  }
+
+  @Test
+  public void testNewArray() throws Exception {
+    runTest("newarray.NewArray");
+  }
+
+  @Test
+  public void testRegAlloc() throws Exception {
+    runTest("regalloc.RegAlloc");
+  }
+
+  @Test
+  public void testReturns() throws Exception {
+    runTest("returns.Returns");
+  }
+
+  @Test
+  public void testStaticField() throws Exception {
+    runTest("staticfield.StaticField");
+  }
+
+  @Test
+  public void testStringBuilding() throws Exception {
+    runTest("stringbuilding.StringBuilding");
+  }
+
+  @Test
+  public void testSwitches() throws Exception {
+    runTest("switches.Switches");
+  }
+
+  @Test
+  public void testSync() throws Exception {
+    runTest("sync.Sync");
+  }
+
+  @Test
+  public void testThrowing() throws Exception {
+    runTest("throwing.Throwing");
+  }
+
+  @Test
+  public void testTrivial() throws Exception {
+    runTest("trivial.Trivial");
+  }
+
+  @Test
+  public void testTryCatch() throws Exception {
+    runTest("trycatch.TryCatch");
+  }
+
+  @Test
+  public void testNestedTryCatches() throws Exception {
+    runTest("nestedtrycatches.NestedTryCatches");
+  }
+
+  @Test
+  public void testTryCatchMany() throws Exception {
+    runTest("trycatchmany.TryCatchMany");
+  }
+
+  @Test
+  public void testInvokeEmpty() throws Exception {
+    runTest("invokeempty.InvokeEmpty");
+  }
+
+  @Test
+  public void testRegress() throws Exception {
+    runTest("regress.Regress");
+  }
+
+  @Test
+  public void testRegress2() throws Exception {
+    runTest("regress2.Regress2");
+  }
+
+  @Test
+  public void testRegress37726195() throws Exception {
+    runTest("regress_37726195.Regress");
+  }
+
+  @Test
+  public void testRegress37658666() throws Exception {
+    runTest(
+        "regress_37658666.Regress",
+        (expectedBytes, actualBytes) -> {
+          // javac emits LDC(-0.0f) instead of the shorter FCONST_0 FNEG emitted by CfConstNumber.
+          String ldc = "mv.visitLdcInsn(new Float(\"-0.0\"));";
+          String constNeg = "mv.visitInsn(FCONST_0);\nmv.visitInsn(FNEG);";
+          assertEquals(asmToString(expectedBytes).replace(ldc, constNeg), asmToString(actualBytes));
+        });
+  }
+
+  @Test
+  public void testRegress37875803() throws Exception {
+    runTest("regress_37875803.Regress");
+  }
+
+  @Test
+  public void testRegress37955340() throws Exception {
+    runTest("regress_37955340.Regress");
+  }
+
+  @Test
+  public void testRegress62300145() throws Exception {
+    runTest("regress_62300145.Regress");
+  }
+
+  @Test
+  public void testRegress64881691() throws Exception {
+    runTest("regress_64881691.Regress");
+  }
+
+  @Test
+  public void testRegress65104300() throws Exception {
+    runTest("regress_65104300.Regress");
+  }
+
+  @Test
+  public void testRegress70703087() throws Exception {
+    runTest("regress_70703087.Test");
+  }
+
+  @Test
+  public void testRegress70736958() throws Exception {
+    runTest("regress_70736958.Test");
+  }
+
+  @Test
+  public void testRegress70737019() throws Exception {
+    runTest("regress_70737019.Test");
+  }
+
+  @Test
+  public void testRegress72361252() throws Exception {
+    runTest("regress_72361252.Test");
+  }
+
+  @Test
+  public void testMemberrebinding2() throws Exception {
+    runTest("memberrebinding2.Memberrebinding");
+  }
+
+  @Test
+  public void testMemberrebinding3() throws Exception {
+    runTest("memberrebinding3.Memberrebinding");
+  }
+
+  @Test
+  public void testMinification() throws Exception {
+    runTest("minification.Minification");
+  }
+
+  @Test
+  public void testEnclosingmethod() throws Exception {
+    runTest("enclosingmethod.Main");
+  }
+
+  @Test
+  public void testEnclosingmethodProguarded() throws Exception {
+    runTest("enclosingmethod_proguarded.Main");
+  }
+
+  @Test
+  public void testInterfaceInlining() throws Exception {
+    runTest("interfaceinlining.Main");
+  }
+
+  @Test
+  public void testSwitchmaps() throws Exception {
+    runTest("switchmaps.Switches");
+  }
+
+  private void runTest(String clazz) throws Exception {
+    runTest(clazz, null);
+  }
+
+  private void runTest(String clazz, BiConsumer<byte[], byte[]> comparator) throws Exception {
+    String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
+    String suffix = "_debuginfo_all";
+    Path inputJar = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, pkg + suffix + JAR_EXTENSION);
     Path outputJar = temp.getRoot().toPath().resolve("output.jar");
     R8Command command =
         R8Command.builder()
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index fc22895..2265c3e 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -3,14 +3,23 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.utils.ListUtils;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
 // Helper to check that a particular error occurred.
-class DiagnosticsChecker implements DiagnosticsHandler {
+public class DiagnosticsChecker implements DiagnosticsHandler {
   public List<Diagnostic> errors = new ArrayList<>();
   public List<Diagnostic> warnings = new ArrayList<>();
   public List<Diagnostic> infos = new ArrayList<>();
@@ -51,4 +60,64 @@
       throw e;
     }
   }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+      int lineStart, int columnStart, String... messageParts) {
+    if (originChecker != null) {
+      originChecker.accept(diagnostic.getOrigin());
+    }
+    TextPosition position;
+    if (diagnostic.getPosition() instanceof TextRange) {
+      position = ((TextRange) diagnostic.getPosition()).getStart();
+    } else {
+      position = ((TextPosition) diagnostic.getPosition());
+    }
+    assertEquals(lineStart, position.getLine());
+    assertEquals(columnStart, position.getColumn());
+    for (String part : messageParts) {
+      assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+          diagnostic.getDiagnosticMessage().contains(part));
+    }
+    return diagnostic;
+  }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+      String... messageParts) {
+    if (originChecker != null) {
+      originChecker.accept(diagnostic.getOrigin());
+    }
+    assertEquals(diagnostic.getPosition(), Position.UNKNOWN);
+    for (String part : messageParts) {
+      assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+          diagnostic.getDiagnosticMessage().contains(part));
+    }
+    return diagnostic;
+  }
+
+  static class PathOriginChecker implements Consumer<Origin> {
+    private final Path path;
+    PathOriginChecker(Path path) {
+      this.path = path;
+    }
+
+    public void accept(Origin origin) {
+      if (path != null) {
+        assertEquals(path, ((PathOrigin) origin).getPath());
+      } else {
+        assertSame(Origin.unknown(), origin);
+      }
+    }
+  }
+
+  public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Path path,
+      int lineStart, int columnStart, String... messageParts) {
+    return checkDiagnostic(diagnostic, new PathOriginChecker(path), lineStart, columnStart,
+        messageParts);
+  }
+
+  public static Diagnostic checkDiagnostics(List<Diagnostic> diagnostics, Path path,
+      int lineStart, int columnStart, String... messageParts) {
+    assertEquals(1, diagnostics.size());
+    return checkDiagnostic(diagnostics.get(0), path, lineStart, columnStart, messageParts);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8CFExamplesTests.java b/src/test/java/com/android/tools/r8/R8CFExamplesTests.java
new file mode 100644
index 0000000..377bfc0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8CFExamplesTests.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.cf.LambdaTest;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.google.common.io.ByteStreams;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class R8CFExamplesTests extends TestBase {
+
+  private static final Path ART_TESTS_DIR = Paths.get(R8RunArtTestsTest.ART_TESTS_DIR, "dx");
+  private final TestMode testMode;
+  private final CompilationMode compilationMode;
+
+  public enum TestMode {
+    CF_SKIP_IR,
+    JAR_TO_IR,
+    CF_TO_IR,
+  }
+
+  @Parameters(name = "{0}:{1}")
+  public static List<Object[]> data() {
+    List<Object[]> data = new ArrayList<>();
+    for (TestMode testMode : TestMode.values()) {
+      for (CompilationMode compilationMode : CompilationMode.values()) {
+        data.add(new Object[] {testMode, compilationMode});
+      }
+    }
+    return data;
+  }
+
+  public R8CFExamplesTests(TestMode testMode, CompilationMode compilationMode) {
+    this.testMode = testMode;
+    this.compilationMode = compilationMode;
+  }
+
+  @Test
+  public void testConstMethodHandle() throws Exception {
+    Path testDirectory = ART_TESTS_DIR.resolve("979-const-method-handle/classes");
+    String classNames[] = {
+      "constmethodhandle.ConstTest", "Main",
+    };
+    runTest(writeInput(testDirectory, classNames), "Main");
+  }
+
+  @Test
+  public void testLambda() throws Exception {
+    runTest(LambdaTest.class);
+  }
+
+  private void runTest(Class<?> clazz) throws Exception {
+    runTest(writeInput(clazz), clazz.getName());
+  }
+
+  private Path writeInput(Class<?> clazz) throws Exception {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(inputJar);
+    String descriptor = DescriptorUtils.javaTypeToDescriptor(clazz.getName());
+    inputConsumer.accept(ToolHelper.getClassAsBytes(clazz), descriptor, null);
+    inputConsumer.finished(null);
+    return inputJar;
+  }
+
+  private Path writeInput(Path testDirectory, String[] classNames) throws Exception {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(inputJar);
+    for (String className : classNames) {
+      Path path = testDirectory.resolve(className.replace('.', '/') + ".class");
+      String descriptor = DescriptorUtils.javaTypeToDescriptor(className);
+      try (InputStream inputStream = new FileInputStream(path.toFile())) {
+        inputConsumer.accept(ByteStreams.toByteArray(inputStream), descriptor, null);
+      }
+    }
+    inputConsumer.finished(null);
+    return inputJar;
+  }
+
+  private void runTest(Path inputJar, String mainClass) throws Exception {
+    ProcessResult runInput = ToolHelper.runJava(inputJar, mainClass);
+    Assert.assertEquals(0, runInput.exitCode);
+    Path outputJar = runR8(inputJar, compilationMode, "output.jar");
+    ProcessResult runOutput = ToolHelper.runJava(outputJar, mainClass);
+    Assert.assertEquals(runInput.toString(), runOutput.toString());
+  }
+
+  private Path runR8(Path inputJar, CompilationMode mode, String outputName) throws Exception {
+    Path outputJar = temp.getRoot().toPath().resolve(outputName);
+    ToolHelper.runR8(
+        R8Command.builder()
+            .addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME))
+            .setMode(mode)
+            .addProgramFiles(inputJar)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .build(),
+        o -> {
+          o.skipIR = this.testMode == TestMode.CF_SKIP_IR;
+          o.enableCfFrontend = this.testMode != TestMode.JAR_TO_IR;
+        });
+    return outputJar;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 923398d..b27569b 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -83,7 +83,7 @@
     D8_AFTER_R8CF
   }
 
-  private static final String ART_TESTS_DIR = "tests/2017-10-04/art";
+  public static final String ART_TESTS_DIR = "tests/2017-10-04/art";
   private static final String ART_LEGACY_TESTS_DIR = "tests/2016-12-19/art/";
   private static final String ART_TESTS_NATIVE_LIBRARY_DIR = "tests/2017-10-04/art/lib64";
   private static final String ART_LEGACY_TESTS_NATIVE_LIBRARY_DIR = "tests/2016-12-19/art/lib64";
@@ -995,6 +995,14 @@
       "625-checker-licm-regressions"
   );
 
+  private static List<String> requireClassInliningToBeDisabled = ImmutableList.of(
+      // Test depends on exception produced for missing method or similar cases, but
+      // after class inlining removes class instantiations and references the exception
+      // is not produced.
+      "042-new-instance",
+      "075-verification-error"
+  );
+
   private static List<String> hasMissingClasses = ImmutableList.of(
       "091-override-package-private-method",
       "003-omnibus-opcodes",
@@ -1085,6 +1093,8 @@
     private final boolean outputMayDiffer;
     // Whether to disable inlining
     private final boolean disableInlining;
+    // Whether to disable class inlining
+    private final boolean disableClassInlining;
     // Has missing classes.
     private final boolean hasMissingClasses;
 
@@ -1102,6 +1112,7 @@
         boolean expectedToFailWithX8,
         boolean outputMayDiffer,
         boolean disableInlining,
+        boolean disableClassInlining,
         boolean hasMissingClasses,
         DexVm dexVm) {
       this.name = name;
@@ -1117,6 +1128,7 @@
       this.expectedToFailWithX8 = expectedToFailWithX8;
       this.outputMayDiffer = outputMayDiffer;
       this.disableInlining = disableInlining;
+      this.disableClassInlining = disableClassInlining;
       this.hasMissingClasses = hasMissingClasses;
     }
 
@@ -1142,6 +1154,7 @@
           false,
           false,
           disableInlining,
+          true, // Disable class inlining for JCTF tests.
           false,
           dexVm);
     }
@@ -1306,6 +1319,7 @@
                 expectedToFailWithCompilerSet.contains(name),
                 outputMayDiffer.contains(name),
                 requireInliningToBeDisabled.contains(name),
+                requireClassInliningToBeDisabled.contains(name),
                 hasMissingClasses.contains(name),
                 dexVm));
       }
@@ -1372,11 +1386,12 @@
       String resultPath,
       CompilationMode compilationMode,
       boolean disableInlining,
+      boolean disableClassInlining,
       boolean hasMissingClasses)
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
       CompilationFailedException {
     executeCompilerUnderTest(compilerUnderTest, fileNames, resultPath, compilationMode, null,
-        disableInlining, hasMissingClasses);
+        disableInlining, disableClassInlining, hasMissingClasses);
   }
 
   private void executeCompilerUnderTest(
@@ -1385,7 +1400,9 @@
       String resultPath,
       CompilationMode mode,
       String keepRulesFile,
-      boolean disableInlining, boolean hasMissingClasses)
+      boolean disableInlining,
+      boolean disableClassInlining,
+      boolean hasMissingClasses)
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
         CompilationFailedException {
     assert mode != null;
@@ -1523,6 +1540,9 @@
                 if (disableInlining) {
                   options.enableInlining = false;
                 }
+                if (disableClassInlining) {
+                  options.enableClassInlining = false;
+                }
                 options.lineNumberOptimization = LineNumberOptimization.OFF;
                 // Some tests actually rely on missing classes for what they test.
                 options.ignoreMissingClasses = hasMissingClasses;
@@ -1734,7 +1754,8 @@
       throws IOException, ProguardRuleParserException, ExecutionException, CompilationException,
       CompilationFailedException {
     executeCompilerUnderTest(compilerUnderTest, fileNames, resultDir.getAbsolutePath(), mode,
-        specification.disableInlining, specification.hasMissingClasses);
+        specification.disableInlining, specification.disableClassInlining,
+        specification.hasMissingClasses);
 
     if (!ToolHelper.artSupported()) {
       return;
@@ -1907,7 +1928,8 @@
       try {
         executeCompilerUnderTest(
             compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-            specification.disableInlining, specification.hasMissingClasses);
+            specification.disableInlining, specification.disableClassInlining,
+            specification.hasMissingClasses);
       } catch (CompilationException | CompilationFailedException e) {
         throw new CompilationError(e.getMessage(), e);
       } catch (ExecutionException e) {
@@ -1919,13 +1941,15 @@
       expectException(Throwable.class);
       executeCompilerUnderTest(
           compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-          specification.disableInlining, specification.hasMissingClasses);
+          specification.disableInlining, specification.disableClassInlining,
+          specification.hasMissingClasses);
       System.err.println("Should have failed R8/D8 compilation with an exception.");
       return;
     } else {
       executeCompilerUnderTest(
           compilerUnderTest, fileNames, resultDir.getCanonicalPath(), compilationMode,
-          specification.disableInlining, specification.hasMissingClasses);
+          specification.disableInlining, specification.disableClassInlining,
+          specification.hasMissingClasses);
     }
 
     if (!specification.skipArt && ToolHelper.artSupported()) {
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 9d45f6a..62dd4af 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -33,6 +33,11 @@
     DX, JAVAC, JAVAC_ALL, JAVAC_NONE
   }
 
+  protected enum Frontend {
+    JAR,
+    CF
+  }
+
   protected enum Output {
     DEX,
     CF
@@ -45,8 +50,20 @@
 
   protected static String[] makeTest(
       Input input, CompilerUnderTest compiler, CompilationMode mode, String clazz, Output output) {
+    return makeTest(input, compiler, mode, clazz, Frontend.JAR, output);
+  }
+
+  protected static String[] makeTest(
+      Input input,
+      CompilerUnderTest compiler,
+      CompilationMode mode,
+      String clazz,
+      Frontend frontend,
+      Output output) {
     String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
-    return new String[] {pkg, input.name(), compiler.name(), mode.name(), clazz, output.name()};
+    return new String[] {
+      pkg, input.name(), compiler.name(), mode.name(), clazz, frontend.name(), output.name()
+    };
   }
 
   @Rule
@@ -57,6 +74,7 @@
   private final CompilationMode mode;
   private final String pkg;
   private final String mainClass;
+  private final Frontend frontend;
   private final Output output;
 
   public R8RunExamplesCommon(
@@ -65,12 +83,14 @@
       String compiler,
       String mode,
       String mainClass,
+      String frontend,
       String output) {
     this.pkg = pkg;
     this.input = Input.valueOf(input);
     this.compiler = CompilerUnderTest.valueOf(compiler);
     this.mode = CompilationMode.valueOf(mode);
     this.mainClass = mainClass;
+    this.frontend = Frontend.valueOf(frontend);
     this.output = Output.valueOf(output);
   }
 
@@ -136,14 +156,24 @@
         break;
       }
       case R8: {
-        R8Command command = addInputFile(R8Command.builder())
-            .setOutput(getOutputFile(), outputMode)
-            .setMode(mode)
-            .build();
-        ExceptionUtils.withR8CompilationHandler(command.getReporter(), () ->
-            ToolHelper.runR8(
-                command,
-                options -> options.lineNumberOptimization = LineNumberOptimization.OFF));
+        R8Command command =
+            addInputFile(R8Command.builder())
+                .setOutput(getOutputFile(), outputMode)
+                .setMode(mode)
+                .build();
+        ExceptionUtils.withR8CompilationHandler(
+            command.getReporter(),
+            () ->
+                ToolHelper.runR8(
+                    command,
+                    options -> {
+                      options.lineNumberOptimization = LineNumberOptimization.OFF;
+                      options.enableCfFrontend = frontend == Frontend.CF;
+                      if (output == Output.CF) {
+                        // Class inliner is not supported with CF backend yet.
+                        options.enableClassInlining = false;
+                      }
+                    }));
         break;
       }
       default:
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
index 02c2d27..97c8624 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
@@ -17,7 +17,7 @@
 @RunWith(Parameterized.class)
 public class R8RunExamplesKotlinTest extends R8RunExamplesCommon {
 
-  @Parameters(name = "{0}_{1}_{2}_{3}_{5}")
+  @Parameters(name = "{0}_{1}_{2}_{3}_{5}_{6}")
   public static Collection<String[]> data() {
     String[] tests = {
         "loops.LoopKt"
@@ -74,7 +74,8 @@
       String compiler,
       String mode,
       String mainClass,
+      String frontend,
       String output) {
-    super(pkg, input, compiler, mode, mainClass, output);
+    super(pkg, input, compiler, mode, mainClass, frontend, output);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index ba036dd..913278a 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -24,7 +24,7 @@
 
   private static final boolean ONLY_RUN_CF_TESTS = false;
 
-  @Parameters(name = "{0}_{1}_{2}_{3}_{5}")
+  @Parameters(name = "{0}_{1}_{2}_{3}_{5}_{6}")
   public static Collection<String[]> data() {
     String[] tests = {
         "arithmetic.Arithmetic",
@@ -99,6 +99,14 @@
       fullTestList.add(
           makeTest(
               Input.JAVAC_ALL, CompilerUnderTest.R8, CompilationMode.RELEASE, test, Output.CF));
+      fullTestList.add(
+          makeTest(
+              Input.JAVAC_ALL,
+              CompilerUnderTest.R8,
+              CompilationMode.RELEASE,
+              test,
+              Frontend.CF,
+              Output.CF));
     }
     return fullTestList;
   }
@@ -109,8 +117,9 @@
       String compiler,
       String mode,
       String mainClass,
+      String frontend,
       String output) {
-    super(pkg, input, compiler, mode, mainClass, output);
+    super(pkg, input, compiler, mode, mainClass, frontend, output);
   }
 
   @Override
@@ -134,7 +143,6 @@
   @Override
   protected Set<String> getFailingCompileCf() {
     return new ImmutableSet.Builder<String>()
-        .add("invoke.Invoke") // outline / CF->IR
         .build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index a29d673..96591d8 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -153,19 +153,26 @@
   protected Path jarTestClasses(Class... classes) throws IOException {
     Path jar = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
     try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
-      for (Class clazz : classes) {
-        try (FileInputStream in =
-            new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
-          out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
-          ByteStreams.copy(in, out);
-          out.closeEntry();
-        }
-      }
+      addTestClassesToJar(out, classes);
     }
     return jar;
   }
 
   /**
+   * Create a temporary JAR file containing the specified test classes.
+   */
+  protected void addTestClassesToJar(JarOutputStream out, Class... classes) throws IOException {
+    for (Class clazz : classes) {
+      try (FileInputStream in =
+          new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
+        out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
+        ByteStreams.copy(in, out);
+        out.closeEntry();
+      }
+    }
+  }
+
+  /**
    * Create a temporary JAR file containing all test classes in a package.
    */
   protected Path jarTestClassesInPackage(Package pkg) throws IOException {
@@ -430,20 +437,6 @@
   }
 
   /**
-   * Run application on Art with the specified main class.
-   */
-  protected String runOnArt(AndroidApp app, String mainClass) throws IOException {
-    return runOnArtRaw(app, mainClass).stdout;
-  }
-
-  /**
-   * Run application on Art with the specified main class.
-   */
-  protected String runOnArt(AndroidApp app, Class mainClass) throws IOException {
-    return runOnArtRaw(app, mainClass).stdout;
-  }
-
-  /**
    * Run application on Art with the specified main class and provided arguments.
    */
   protected String runOnArt(AndroidApp app, Class mainClass, String... args) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/cf/BaseDefaultMethodTestRunner.java b/src/test/java/com/android/tools/r8/cf/BaseDefaultMethodTestRunner.java
index 414752f..be00acf 100644
--- a/src/test/java/com/android/tools/r8/cf/BaseDefaultMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/BaseDefaultMethodTestRunner.java
@@ -46,8 +46,7 @@
     for (Class<?> c : CLASSES) {
       builder.addClassProgramData(ToolHelper.getClassAsBytes(c), Origin.unknown());
     }
-    // TODO(b/75997473): Enable inlining when supported
-    ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
+    ToolHelper.runR8(builder.build());
     ProcessResult runOutput = ToolHelper.runJava(out, CLASS.getCanonicalName());
     assertEquals(runInput.toString(), runOutput.toString());
   }
diff --git a/src/test/java/com/android/tools/r8/cf/BootstrapTest.java b/src/test/java/com/android/tools/r8/cf/BootstrapTest.java
index 0f92fb3..7fdad0d 100644
--- a/src/test/java/com/android/tools/r8/cf/BootstrapTest.java
+++ b/src/test/java/com/android/tools/r8/cf/BootstrapTest.java
@@ -56,10 +56,7 @@
 
     @Override
     public String toString() {
-      // TODO(mathiasr): Add pgMap to output when resource API (go/r8g/19460) has landed.
-      // Without resource API, comparing pgMaps will fail because R8 does not keep the resource
-      // indicating which Git revision R8 was compiled from.
-      return processResult.toString();
+      return processResult.toString() + "\n\n" + pgMap;
     }
   }
 
@@ -71,37 +68,50 @@
     assertEquals(0, runHello.exitCode);
 
     // Run r8.jar on hello.jar to ensure that r8.jar is a working compiler.
-    R8Result runInputR8 = runExternalR8(R8_STABLE_JAR, hello, "input", KEEP_HELLO);
+    R8Result runInputR8 = runExternalR8(R8_STABLE_JAR, hello, "input", KEEP_HELLO, "--debug");
     ProcessResult runHelloR8 = ToolHelper.runJava(runInputR8.outputJar, "hello.Hello");
     assertEquals(runHello.toString(), runHelloR8.toString());
 
+    compareR8(hello, runInputR8, CompilationMode.RELEASE, "r8-r8-rel", "--release", "output-rel");
+    compareR8(hello, runInputR8, CompilationMode.DEBUG, "r8-r8", "--debug", "output");
+  }
+
+  private void compareR8(
+      Path hello,
+      R8Result runInputR8,
+      CompilationMode internalMode,
+      String internalOutput,
+      String externalMode,
+      String externalOutput)
+      throws Exception {
     // Run R8 on r8.jar, and run the resulting compiler on hello.jar.
-    Path output = runR8(R8_STABLE_JAR, "r8-r8", KEEP_R8);
-    R8Result runR8R8 = runExternalR8(output, hello, "output", KEEP_HELLO);
+    Path output = runR8(R8_STABLE_JAR, internalOutput, KEEP_R8, internalMode);
+    R8Result runR8R8 = runExternalR8(output, hello, externalOutput, KEEP_HELLO, externalMode);
     // Check that the process outputs (exit code, stdout, stderr) are the same.
     assertEquals(runInputR8.toString(), runR8R8.toString());
     // Check that the output jars are the same.
     assertProgramsEqual(runInputR8.outputJar, runR8R8.outputJar);
   }
 
-  private Path runR8(Path inputJar, String outputFolder, String[] keepRules) throws Exception {
+  private Path runR8(Path inputJar, String outputFolder, String[] keepRules, CompilationMode mode)
+      throws Exception {
     Path outputPath = temp.newFolder(outputFolder).toPath();
     Path outputJar = outputPath.resolve("output.jar");
     Path pgConfigFile = outputPath.resolve("keep.rules");
     FileUtils.writeTextFile(pgConfigFile, keepRules);
     ToolHelper.runR8(
         R8Command.builder()
-            .setMode(CompilationMode.DEBUG)
+            .setMode(mode)
             .addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME))
-            // TODO(mathiasr): Add resources to output when resource API (go/r8g/19460) has landed.
-            .setProgramConsumer(new ClassFileConsumer.ArchiveConsumer(outputJar))
+            .setProgramConsumer(new ClassFileConsumer.ArchiveConsumer(outputJar, true))
             .addProgramFiles(inputJar)
             .addProguardConfigurationFiles(pgConfigFile)
             .build());
     return outputJar;
   }
 
-  private R8Result runExternalR8(Path r8Jar, Path inputJar, String outputFolder, String[] keepRules)
+  private R8Result runExternalR8(
+      Path r8Jar, Path inputJar, String outputFolder, String[] keepRules, String mode)
       throws Exception {
     Path outputPath = temp.newFolder(outputFolder).toPath();
     Path pgConfigFile = outputPath.resolve("keep.rules");
@@ -120,7 +130,7 @@
             outputJar.toString(),
             "--pg-conf",
             pgConfigFile.toString(),
-            "--debug",
+            mode,
             "--pg-map-output",
             pgMapFile.toString());
     if (processResult.exitCode != 0) {
diff --git a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
index fed7637..ad6a72b 100644
--- a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
@@ -58,11 +58,6 @@
             .addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()))
             .setProgramConsumer(consumer);
     input.accept(builder);
-    ToolHelper.runR8(
-        builder.build(),
-        o -> {
-          o.invalidDebugInfoFatal = true;
-          o.enableInlining = false;
-        });
+    ToolHelper.runR8(builder.build(), o -> o.invalidDebugInfoFatal = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/cf/LambdaTestRunner.java b/src/test/java/com/android/tools/r8/cf/LambdaTestRunner.java
index f47bdc6..6c9d7b6 100644
--- a/src/test/java/com/android/tools/r8/cf/LambdaTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/LambdaTestRunner.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
@@ -12,8 +11,10 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.JarCode;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
@@ -22,18 +23,10 @@
 import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
+import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.tree.AbstractInsnNode;
-import org.objectweb.asm.tree.ClassNode;
-import org.objectweb.asm.tree.InsnList;
-import org.objectweb.asm.tree.InvokeDynamicInsnNode;
-import org.objectweb.asm.tree.MethodNode;
 
 public class LambdaTestRunner {
 
@@ -49,8 +42,10 @@
     // is not modified by the R8 compilation.
     // First, extract the InvokeDynamic instruction from the input class.
     byte[] inputClass = ToolHelper.getClassAsBytes(CLASS);
-    int opcode = Opcodes.INVOKEDYNAMIC;
-    InvokeDynamicInsnNode insnInput = findFirstInMethod(inputClass, opcode);
+    AndroidApp inputApp =
+        AndroidApp.builder().addClassProgramData(inputClass, Origin.unknown()).build();
+    CfInvokeDynamic insnInput = findFirstInMethod(inputApp);
+    Assert.assertNotNull("No CfInvokeDynamic found in input", insnInput);
     // Compile with R8 and extract the InvokeDynamic instruction from the output class.
     AndroidAppConsumers appBuilder = new AndroidAppConsumers();
     Path outPath = temp.getRoot().toPath().resolve("out.jar");
@@ -61,13 +56,11 @@
             .setProgramConsumer(appBuilder.wrapClassFileConsumer(new ArchiveConsumer(outPath)))
             .addClassProgramData(inputClass, Origin.unknown())
             .build());
-    AndroidApp app = appBuilder.build();
-    InvokeDynamicInsnNode insnOutput = findFirstInMethod(app, opcode);
+    AndroidApp outputApp = appBuilder.build();
+    CfInvokeDynamic insnOutput = findFirstInMethod(outputApp);
+    Assert.assertNotNull("No CfInvokeDynamic found in output", insnOutput);
     // Check that the InvokeDynamic instruction is not modified.
-    assertEquals(insnInput.name, insnOutput.name);
-    assertEquals(insnInput.desc, insnOutput.desc);
-    assertEquals(insnInput.bsm, insnOutput.bsm);
-    assertArrayEquals(insnInput.bsmArgs, insnOutput.bsmArgs);
+    assertEquals(print(insnInput), print(insnOutput));
     // Check that execution gives the same output.
     ProcessResult inputResult =
         ToolHelper.runJava(ToolHelper.getClassPathForTests(), CLASS.getName());
@@ -75,45 +68,24 @@
     assertEquals(inputResult.toString(), outputResult.toString());
   }
 
-  private InvokeDynamicInsnNode findFirstInMethod(AndroidApp app, int opcode) throws Exception {
+  private static CfInvokeDynamic findFirstInMethod(AndroidApp app) throws Exception {
     String returnType = "void";
-    DexInspector inspector = new DexInspector(app);
+    DexInspector inspector = new DexInspector(app, o -> o.enableCfFrontend = true);
     List<String> args = Collections.singletonList(String[].class.getTypeName());
     DexEncodedMethod method = inspector.clazz(CLASS).method(returnType, METHOD, args).getMethod();
-    JarCode jarCode = method.getCode().asJarCode();
-    MethodNode outputMethod = jarCode.getNode();
-    return (InvokeDynamicInsnNode) findFirstInstruction(outputMethod, opcode);
-  }
-
-  private InvokeDynamicInsnNode findFirstInMethod(byte[] clazz, int opcode) {
-    MethodNode[] method = {null};
-    new ClassReader(clazz)
-        .accept(
-            new ClassNode(Opcodes.ASM6) {
-              @Override
-              public MethodVisitor visitMethod(
-                  int access, String name, String desc, String signature, String[] exceptions) {
-                if (name.equals(METHOD)) {
-                  method[0] = new MethodNode(access, name, desc, signature, exceptions);
-                  return method[0];
-                } else {
-                  return null;
-                }
-              }
-            },
-            0);
-    return (InvokeDynamicInsnNode) findFirstInstruction(method[0], opcode);
-  }
-
-  private AbstractInsnNode findFirstInstruction(MethodNode node, int opcode) {
-    assert node != null;
-    InsnList asmInsns = node.instructions;
-    for (ListIterator<AbstractInsnNode> it = asmInsns.iterator(); it.hasNext(); ) {
-      AbstractInsnNode insn = it.next();
-      if (insn.getOpcode() == opcode) {
-        return insn;
+    CfCode code = method.getCode().asCfCode();
+    for (CfInstruction instruction : code.getInstructions()) {
+      if (instruction instanceof CfInvokeDynamic) {
+        return (CfInvokeDynamic) instruction;
       }
     }
-    throw new RuntimeException("Instruction not found");
+    return null;
   }
+
+  private static String print(CfInstruction instruction) {
+    CfPrinter printer = new CfPrinter();
+    instruction.print(printer);
+    return printer.toString();
+  }
+
 }
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index ae3b95e..e43ffd7 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -9,55 +9,91 @@
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.ProgramConsumer;
-import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Reporter;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
-import org.junit.Rule;
+import java.util.List;
+import org.junit.Assume;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
-public class MethodHandleTestRunner {
+@RunWith(Parameterized.class)
+public class MethodHandleTestRunner extends TestBase {
   static final Class<?> CLASS = MethodHandleTest.class;
 
-  private boolean ldc = false;
-  private boolean minify = false;
+  enum LookupType {
+    DYNAMIC,
+    CONSTANT,
+  }
 
-  @Rule
-  public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+  enum MinifyMode {
+    NONE,
+    MINIFY,
+  }
 
-  @Test
-  public void testMethodHandlesLookup() throws Exception {
-    // Run test with dynamic method lookups, i.e. using MethodHandles.lookup().find*()
-    ldc = false;
-    test();
+  enum Frontend {
+    JAR,
+    CF,
+  }
+
+  private CompilationMode compilationMode;
+  private LookupType lookupType;
+  private Frontend frontend;
+  private ProcessResult runInput;
+  private MinifyMode minifyMode;
+
+  @Parameters(name = "{0}_{1}_{2}_{3}")
+  public static List<String[]> data() {
+    List<String[]> res = new ArrayList<>();
+    for (LookupType lookupType : LookupType.values()) {
+      for (Frontend frontend : Frontend.values()) {
+        for (MinifyMode minifyMode : MinifyMode.values()) {
+          if (lookupType == LookupType.DYNAMIC && minifyMode == MinifyMode.MINIFY) {
+            // Skip because we don't keep the members looked up dynamically.
+            continue;
+          }
+          for (CompilationMode compilationMode : CompilationMode.values()) {
+            res.add(
+                new String[] {
+                  lookupType.name(), frontend.name(), minifyMode.name(), compilationMode.name()
+                });
+          }
+        }
+      }
+    }
+    return res;
+  }
+
+  public MethodHandleTestRunner(
+      String lookupType, String frontend, String minifyMode, String compilationMode) {
+    this.lookupType = LookupType.valueOf(lookupType);
+    this.frontend = Frontend.valueOf(frontend);
+    this.minifyMode = MinifyMode.valueOf(minifyMode);
+    this.compilationMode = CompilationMode.valueOf(compilationMode);
   }
 
   @Test
-  public void testLdcMethodHandle() throws Exception {
-    // Run test with LDC methods, i.e. without java.lang.invoke.MethodHandles
-    ldc = true;
-    test();
-  }
-
-  @Test
-  public void testMinify() throws Exception {
-    // Run test with LDC methods, i.e. without java.lang.invoke.MethodHandles
-    ldc = true;
-    ProcessResult runInput = runInput();
-    assertEquals(0, runInput.exitCode);
-    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
-    build(new ClassFileConsumer.ArchiveConsumer(outCf), true);
-    ProcessResult runCf =
-        ToolHelper.runJava(outCf, CLASS.getCanonicalName(), ldc ? "error" : "exception");
-    assertEquals(runInput.toString(), runCf.toString());
+  public void test() throws Exception {
+    runInput();
+    runCf();
+    // TODO(mathiasr): Once we include a P runtime, change this to "P and above".
+    if (ToolHelper.getDexVm() == DexVm.ART_DEFAULT && ToolHelper.artSupported()) {
+      runDex();
+    }
   }
 
   private final Class[] inputClasses = {
@@ -70,25 +106,39 @@
     MethodHandleTest.F.class,
   };
 
-  private void test() throws Exception {
-    ProcessResult runInput = runInput();
-    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
-    build(new ClassFileConsumer.ArchiveConsumer(outCf), false);
-    Path outDex = temp.getRoot().toPath().resolve("dex.zip");
-    build(new DexIndexedConsumer.ArchiveConsumer(outDex), false);
-
-    ProcessResult runCf =
-        ToolHelper.runJava(outCf, CLASS.getCanonicalName(), ldc ? "error" : "exception");
-    assertEquals(runInput.toString(), runCf.toString());
-    // TODO(mathiasr): Once we include a P runtime, change this to "P and above".
-    if (ToolHelper.getDexVm() != DexVm.ART_DEFAULT) {
-      return;
+  private void runInput() throws Exception {
+    Path out = temp.getRoot().toPath().resolve("input.jar");
+    ClassFileConsumer.ArchiveConsumer archiveConsumer = new ClassFileConsumer.ArchiveConsumer(out);
+    for (Class<?> c : inputClasses) {
+      archiveConsumer.accept(
+          getClassAsBytes(c), DescriptorUtils.javaTypeToDescriptor(c.getName()), null);
     }
+    archiveConsumer.finished(null);
+    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
+    runInput = ToolHelper.runJava(out, CLASS.getName(), expected);
+    if (runInput.exitCode != 0) {
+      System.out.println(runInput);
+    }
+    assertEquals(0, runInput.exitCode);
+  }
+
+  private void runCf() throws Exception {
+    Path outCf = temp.getRoot().toPath().resolve("cf.jar");
+    build(new ClassFileConsumer.ArchiveConsumer(outCf));
+    String expected = lookupType == LookupType.CONSTANT ? "error" : "exception";
+    ProcessResult runCf = ToolHelper.runJava(outCf, CLASS.getCanonicalName(), expected);
+    assertEquals(runInput.toString(), runCf.toString());
+  }
+
+  private void runDex() throws Exception {
+    Path outDex = temp.getRoot().toPath().resolve("dex.zip");
+    build(new DexIndexedConsumer.ArchiveConsumer(outDex));
+    String expected = lookupType == LookupType.CONSTANT ? "pass" : "exception";
     ProcessResult runDex =
         ToolHelper.runArtRaw(
             outDex.toString(),
             CLASS.getCanonicalName(),
-            cmd -> cmd.appendProgramArgument(ldc ? "pass" : "exception"));
+            cmd -> cmd.appendProgramArgument(expected));
     // Only compare stdout and exitCode since dex2oat prints to stderr.
     if (runInput.exitCode != runDex.exitCode) {
       System.out.println(runDex.stderr);
@@ -97,51 +147,49 @@
     assertEquals(runInput.exitCode, runDex.exitCode);
   }
 
-  private void build(ProgramConsumer programConsumer, boolean minify) throws Exception {
+  private void build(ProgramConsumer programConsumer) throws Exception {
     // MethodHandle.invoke() only supported from Android O
     // ConstMethodHandle only supported from Android P
     AndroidApiLevel apiLevel = AndroidApiLevel.P;
-    Builder cfBuilder =
+    Builder command =
         R8Command.builder()
-            .setMode(CompilationMode.DEBUG)
+            .setMode(compilationMode)
             .addLibraryFiles(ToolHelper.getAndroidJar(apiLevel))
             .setProgramConsumer(programConsumer);
     if (!(programConsumer instanceof ClassFileConsumer)) {
-      cfBuilder.setMinApiLevel(apiLevel.getLevel());
+      command.setMinApiLevel(apiLevel.getLevel());
     }
     for (Class<?> c : inputClasses) {
       byte[] classAsBytes = getClassAsBytes(c);
-      cfBuilder.addClassProgramData(classAsBytes, Origin.unknown());
+      command.addClassProgramData(classAsBytes, Origin.unknown());
     }
-    if (minify) {
-      cfBuilder.addProguardConfiguration(
+    if (minifyMode == MinifyMode.MINIFY) {
+      command.addProguardConfiguration(
           Arrays.asList(
               "-keep public class com.android.tools.r8.cf.MethodHandleTest {",
               "  public static void main(...);",
               "}"),
           Origin.unknown());
     }
-    R8.run(cfBuilder.build());
-  }
-
-  private ProcessResult runInput() throws Exception {
-    Path out = temp.getRoot().toPath().resolve("input.jar");
-    ClassFileConsumer.ArchiveConsumer archiveConsumer = new ClassFileConsumer.ArchiveConsumer(out);
-    for (Class<?> c : inputClasses) {
-      archiveConsumer.accept(
-          getClassAsBytes(c), DescriptorUtils.javaTypeToDescriptor(c.getName()), null);
+    try {
+      ToolHelper.runR8(
+          command.build(), options -> options.enableCfFrontend = frontend == Frontend.CF);
+    } catch (CompilationError e) {
+      if (frontend == Frontend.CF && compilationMode == CompilationMode.DEBUG) {
+        // TODO(b/79725635): Investigate why these tests fail on the buildbot.
+        // Use a Reporter to extract origin info to standard error.
+        new Reporter(new DefaultDiagnosticsHandler()).error(e);
+        // Print the stack trace since this is not always printed by JUnit.
+        e.printStackTrace();
+        Assume.assumeNoException(
+            "TODO(b/79725635): Investigate why these tests fail on the buildbot.", e);
+      }
+      throw e;
     }
-    archiveConsumer.finished(null);
-    ProcessResult runInput = ToolHelper.runJava(out, CLASS.getName(), ldc ? "error" : "exception");
-    if (runInput.exitCode != 0) {
-      System.out.println(runInput);
-    }
-    assertEquals(0, runInput.exitCode);
-    return runInput;
   }
 
   private byte[] getClassAsBytes(Class<?> clazz) throws Exception {
-    if (ldc) {
+    if (lookupType == LookupType.CONSTANT) {
       if (clazz == MethodHandleTest.D.class) {
         return MethodHandleDump.dumpD();
       } else if (clazz == MethodHandleTest.class) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index e256ae4..c6a2bd3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -39,6 +39,7 @@
 
   private void configure(InternalOptions options) {
     options.enableClassMerging = true;
+    options.enableClassInlining = false;
   }
 
   private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
diff --git a/src/test/java/com/android/tools/r8/code/ClassWithNativeMethodTest.java b/src/test/java/com/android/tools/r8/code/ClassWithNativeMethodTest.java
deleted file mode 100644
index 293485a..0000000
--- a/src/test/java/com/android/tools/r8/code/ClassWithNativeMethodTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.code;
-
-import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertThat;
-
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.VmTestRunner;
-import com.android.tools.r8.jasmin.JasminBuilder;
-import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
-import com.android.tools.r8.utils.AndroidApp;
-import com.android.tools.r8.utils.DexInspector;
-import com.android.tools.r8.utils.DexInspector.ClassSubject;
-import com.android.tools.r8.utils.DexInspector.MethodSubject;
-import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(VmTestRunner.class)
-public class ClassWithNativeMethodTest extends TestBase {
-
-  @Test
-  public void test() throws Exception {
-    JasminBuilder jasminBuilder = new JasminBuilder();
-
-    ClassBuilder cls = jasminBuilder.addClass("Main");
-    cls.addDefaultConstructor();
-    cls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
-        ".limit stack 3",
-        ".limit locals 2",
-        "getstatic java/lang/System/out Ljava/io/PrintStream;",
-        "aload_1",
-        "invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
-        "return");
-    cls.addNativeMethod("n1", ImmutableList.of("Ljava/lang/String;"), "V",
-        "return");
-    cls.addNativeMethod("n2", ImmutableList.of("Ljava/lang/String;"), "V",
-        "return");
-    cls.addNativeMethod("n3", ImmutableList.of("Ljava/lang/String;"), "V",
-        "return");
-    cls.addNativeMethod("n4", ImmutableList.of("Ljava/lang/String;"), "V",
-        "return");
-    cls.addMainMethod(
-        ".limit stack 4",
-        ".limit locals 2",
-        "new " + cls.name,
-        "dup",
-        "invokespecial " + cls.name + "/<init>()V",
-        "astore_0",
-        "aload_0",
-        "ldc \"foo\"",
-        "invokevirtual " + cls.name + "/foo(Ljava/lang/String;)V",
-        "return");
-
-    String mainClassName = cls.name;
-
-    Path outputDirectory = temp.newFolder().toPath();
-    jasminBuilder.writeClassFiles(outputDirectory);
-    ProcessResult javaResult = ToolHelper.runJava(outputDirectory, mainClassName);
-    // Native .o is not given. Expect to see JNI error.
-    assertNotEquals(0, javaResult.exitCode);
-    assertThat(javaResult.stderr, containsString("JNI"));
-
-    AndroidApp processedApp = compileWithD8(jasminBuilder.build());
-
-    DexInspector inspector = new DexInspector(processedApp);
-    ClassSubject mainSubject = inspector.clazz(cls.name);
-    MethodSubject nativeMethod =
-        mainSubject.method("void", "n1", ImmutableList.of("java.lang.String"));
-    assertThat(nativeMethod, isPresent());
-
-    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    // ART can process main() even without native definitions.
-    assertEquals(0, artResult.exitCode);
-    assertThat(artResult.stdout, containsString("foo"));
-  }
-
-
-}
diff --git a/src/test/java/com/android/tools/r8/code/NativeMethodWithCodeTest.java b/src/test/java/com/android/tools/r8/code/NativeMethodWithCodeTest.java
new file mode 100644
index 0000000..83e711c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/code/NativeMethodWithCodeTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.code;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class NativeMethodWithCodeTest extends TestBase {
+
+  // Test that D8 removes code from native methods (to match the behavior of dx).
+  // Note that the JVM rejects a class if it has a native method with a code attribute,
+  // but D8 has to handle that and cannot simply throw an error even though the JVM does.
+
+  @Test
+  public void test() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder cls = jasminBuilder.addClass("Main");
+    cls.addDefaultConstructor();
+    cls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "aload_1",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
+        "return");
+    cls.addNativeMethodWithCode("n1", ImmutableList.of("Ljava/lang/String;"), "V",
+        "return");
+    cls.addNativeMethodWithCode("n2", ImmutableList.of("Ljava/lang/String;"), "V",
+        "return");
+    cls.addNativeMethodWithCode("n3", ImmutableList.of("Ljava/lang/String;"), "V",
+        "return");
+    cls.addNativeMethodWithCode("n4", ImmutableList.of("Ljava/lang/String;"), "V",
+        "return");
+    cls.addMainMethod(
+        ".limit stack 4",
+        ".limit locals 2",
+        "new " + cls.name,
+        "dup",
+        "invokespecial " + cls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "ldc \"foo\"",
+        "invokevirtual " + cls.name + "/foo(Ljava/lang/String;)V",
+        "return");
+
+    String mainClassName = cls.name;
+
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    jasminBuilder.writeJar(inputJar, null);
+    AndroidApp inputApp = AndroidApp.builder().addProgramFiles(inputJar).build();
+    // TODO(mathiasr): Consider making frontend read in code on native methods.
+    // If we do that, this assertNull should change to assertNotNull.
+    assertNull(getNativeMethod(mainClassName, inputApp).getMethod().getCode());
+
+    // JVM throws ClassFormatError because the input contains code on a native method. (JVM8§4.7.3)
+    ProcessResult javaResult = ToolHelper.runJava(inputJar, mainClassName);
+    assertNotEquals(0, javaResult.exitCode);
+    assertThat(javaResult.stderr, containsString("ClassFormatError"));
+
+    // DX strips code from native methods.
+    Path dxOutputDir = temp.newFolder().toPath();
+    Path dxOutputFile = dxOutputDir.resolve("classes.dex");
+    ToolHelper.runDexer(inputJar.toString(), dxOutputDir.toString());
+    AndroidApp dxApp = AndroidApp.builder().addProgramFiles(dxOutputFile).build();
+    assertNull(getNativeMethod(mainClassName, dxApp).getMethod().getCode());
+
+    // D8 should also strip code from native methods.
+    AndroidApp processedApp = compileWithD8(inputApp);
+    assertThat(getNativeMethod(mainClassName, processedApp), isPresent());
+    // TODO(mathiasr): Consider making D8 maintain code on native methods.
+    // If we do that, this assertNull should change to assertNotNull.
+    assertNull(getNativeMethod(mainClassName, processedApp).getMethod().getCode());
+
+    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
+    assertEquals(0, artResult.exitCode);
+    assertThat(artResult.stdout, containsString("foo"));
+  }
+
+  private MethodSubject getNativeMethod(String mainClassName, AndroidApp processedApp)
+      throws IOException, ExecutionException {
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject mainSubject = inspector.clazz(mainClassName);
+    return mainSubject.method("void", "n1", ImmutableList.of("java.lang.String"));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
index 45e3c96..eca0441 100644
--- a/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
@@ -51,8 +51,9 @@
       .add(KotlinD8Config.DEBUGGEE_KOTLIN_JAR, ContinuousSteppingTest::allVersions)
       .addAll(findAllJarsIn(Paths.get(ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR)),
           ContinuousSteppingTest::fromAndroidN)
-      .addAll(findAllJarsIn(Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR)),
-          ContinuousSteppingTest::fromAndroidO)
+      // TODO(b/79911828) Investigate timeout issues for Android O examples.
+      //  .addAll(findAllJarsIn(Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR)),
+      //      ContinuousSteppingTest::fromAndroidO)
       .build();
 
   private static final Map<Path, DebugTestConfig> compiledJarConfig = new HashMap<>();
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index e8d21f2..25356ae 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -390,6 +390,14 @@
     return inspect(t -> t.checkLocal(localName));
   }
 
+  protected final JUnit3Wrapper.Command checkLocals(String... localNames) {
+    return inspect(t -> {
+      for (String str : localNames) {
+        t.checkLocal(str);
+      }
+    });
+  }
+
   protected final JUnit3Wrapper.Command checkLocal(String localName, Value expectedValue) {
     return inspect(t -> t.checkLocal(localName, expectedValue));
   }
@@ -398,6 +406,14 @@
     return inspect(t -> t.checkNoLocal(localName));
   }
 
+  protected final JUnit3Wrapper.Command checkNoLocals(String... localNames) {
+    return inspect(t -> {
+      for (String str : localNames) {
+        t.checkNoLocal(str);
+      }
+    });
+  }
+
   protected final JUnit3Wrapper.Command checkNoLocal() {
     return inspect(t -> {
       List<String> localNames = t.getLocalNames();
diff --git a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
index 913d39e..02e995f 100644
--- a/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ExamplesDebugTest.java
@@ -10,57 +10,375 @@
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 import org.junit.Assume;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class ExamplesDebugTest extends DebugTestBase {
 
-  private Path getExampleJar(String pkg) {
-    return Paths.get(
-        ToolHelper.EXAMPLES_BUILD_DIR, pkg + "_debuginfo_all" + FileUtils.JAR_EXTENSION);
+  private String clazzName;
+  private Path inputJar;
+
+  private Stream<DebuggeeState> input() throws Exception {
+    return streamDebugTest(new CfDebugTestConfig(inputJar), clazzName, ANDROID_FILTER);
   }
 
-  public DebugTestConfig cfConfig(String pkg) throws Exception {
-    return new CfDebugTestConfig(getExampleJar(pkg));
+  private Stream<DebuggeeState> d8() throws Exception {
+    D8DebugTestConfig config = new D8DebugTestConfig().compileAndAdd(temp, inputJar);
+    return streamDebugTest(config, clazzName, ANDROID_FILTER);
   }
 
-  public DebugTestConfig d8Config(String pkg) throws Exception {
-    return new D8DebugTestConfig().compileAndAdd(temp, getExampleJar(pkg));
+  private Stream<DebuggeeState> r8jar() throws Exception {
+    return streamDebugTest(getCfConfig("r8jar.jar", o -> {}), clazzName, ANDROID_FILTER);
   }
 
-  public DebugTestConfig r8DebugCfConfig(String pkg) throws Exception {
-    Path input = getExampleJar(pkg);
-    Path output = temp.newFolder().toPath().resolve("r8_debug_cf_output.jar");
+  private Stream<DebuggeeState> r8cf() throws Exception {
+    return streamDebugTest(
+        getCfConfig("r8cf.jar", options -> options.enableCfFrontend = true),
+        clazzName,
+        ANDROID_FILTER);
+  }
+
+  private DebugTestConfig getCfConfig(String outputName, Consumer<InternalOptions> optionsConsumer)
+      throws Exception {
+    Path input = inputJar;
+    Path output = temp.newFolder().toPath().resolve(outputName);
     ToolHelper.runR8(
         R8Command.builder()
             .addProgramFiles(input)
             .setMode(CompilationMode.DEBUG)
             .setOutput(output, OutputMode.ClassFile)
-            .build());
+            .build(),
+        optionsConsumer);
     return new CfDebugTestConfig(output);
   }
 
   @Test
   public void testArithmetic() throws Throwable {
+    testDebugging("arithmetic", "Arithmetic");
+  }
+
+  @Test
+  public void testArrayAccess() throws Exception {
+    testDebugging("arrayaccess", "ArrayAccess");
+  }
+
+  @Test
+  public void testBArray() throws Exception {
+    testDebugging("barray", "BArray");
+  }
+
+  @Test
+  public void testBridgeMethod() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("bridge", "BridgeMethod");
+  }
+
+  @Test
+  public void testCommonSubexpressionElimination() throws Exception {
+    testDebugging("cse", "CommonSubexpressionElimination");
+  }
+
+  @Test
+  public void testConstants() throws Exception {
+    testDebugging("constants", "Constants");
+  }
+
+  @Test
+  public void testControlFlow() throws Exception {
+    testDebugging("controlflow", "ControlFlow");
+  }
+
+  @Test
+  public void testConversions() throws Exception {
+    testDebugging("conversions", "Conversions");
+  }
+
+  @Test
+  public void testFloatingPointValuedAnnotation() throws Exception {
+    // D8 has no source file.
+    testDebuggingJvmOnly("floating_point_annotations", "FloatingPointValuedAnnotationTest");
+  }
+
+  @Test
+  public void testFilledArray() throws Exception {
+    testDebugging("filledarray", "FilledArray");
+  }
+
+  @Test
+  public void testHello() throws Exception {
+    testDebugging("hello", "Hello");
+  }
+
+  @Test
+  public void testIfStatements() throws Exception {
+    testDebugging("ifstatements", "IfStatements");
+  }
+
+  @Test
+  public void testInstanceVariable() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("instancevariable", "InstanceVariable");
+  }
+
+  @Test
+  public void testInstanceofString() throws Exception {
+    testDebugging("instanceofstring", "InstanceofString");
+  }
+
+  @Test
+  public void testInvoke() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("invoke", "Invoke");
+  }
+
+  @Test
+  public void testJumboString() throws Exception {
+    testDebugging("jumbostring", "JumboString");
+  }
+
+  @Test
+  public void testLoadConst() throws Exception {
+    testDebugging("loadconst", "LoadConst");
+  }
+
+  @Test
+  public void testUdpServer() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("loop", "UdpServer");
+  }
+
+  @Test
+  public void testNewArray() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("newarray", "NewArray");
+  }
+
+  @Test
+  public void testRegAlloc() throws Exception {
+    testDebugging("regalloc", "RegAlloc");
+  }
+
+  @Test
+  public void testReturns() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("returns", "Returns");
+  }
+
+  @Test
+  public void testStaticField() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("staticfield", "StaticField");
+  }
+
+  @Test
+  public void testStringBuilding() throws Exception {
+    testDebugging("stringbuilding", "StringBuilding");
+  }
+
+  @Test
+  public void testSwitches() throws Exception {
+    testDebugging("switches", "Switches");
+  }
+
+  @Test
+  public void testSync() throws Exception {
+    // D8 has two local variables with empty names.
+    testDebuggingJvmOnly("sync", "Sync");
+  }
+
+  @Test
+  public void testThrowing() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("throwing", "Throwing");
+  }
+
+  @Test
+  public void testTrivial() throws Exception {
+    testDebugging("trivial", "Trivial");
+  }
+
+  @Ignore("TODO(mathiasr): InvalidDebugInfoException in CfSourceCode")
+  @Test
+  public void testTryCatch() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("trycatch", "TryCatch");
+  }
+
+  @Test
+  public void testNestedTryCatches() throws Exception {
+    testDebugging("nestedtrycatches", "NestedTryCatches");
+  }
+
+  @Test
+  public void testTryCatchMany() throws Exception {
+    testDebugging("trycatchmany", "TryCatchMany");
+  }
+
+  @Test
+  public void testInvokeEmpty() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("invokeempty", "InvokeEmpty");
+  }
+
+  @Test
+  public void testRegress() throws Exception {
+    testDebugging("regress", "Regress");
+  }
+
+  @Test
+  public void testRegress2() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("regress2", "Regress2");
+  }
+
+  @Ignore("TODO(mathiasr): Different behavior CfSourceCode vs JarSourceCode")
+  @Test
+  public void testRegress37726195() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("regress_37726195", "Regress");
+  }
+
+  @Test
+  public void testRegress37658666() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("regress_37658666", "Regress");
+  }
+
+  @Test
+  public void testRegress37875803() throws Exception {
+    testDebugging("regress_37875803", "Regress");
+  }
+
+  @Test
+  public void testRegress37955340() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("regress_37955340", "Regress");
+  }
+
+  @Test
+  public void testRegress62300145() throws Exception {
+    // D8 has no source file.
+    testDebuggingJvmOnly("regress_62300145", "Regress");
+  }
+
+  @Test
+  public void testRegress64881691() throws Exception {
+    testDebugging("regress_64881691", "Regress");
+  }
+
+  @Test
+  public void testRegress65104300() throws Exception {
+    testDebugging("regress_65104300", "Regress");
+  }
+
+  @Ignore("TODO(b/79671093): This test seems to take forever")
+  @Test
+  public void testRegress70703087() throws Exception {
+    testDebugging("regress_70703087", "Test");
+  }
+
+  @Test
+  public void testRegress70736958() throws Exception {
+    // D8 has a local variable with empty name.
+    testDebuggingJvmOnly("regress_70736958", "Test");
+  }
+
+  @Ignore("TODO(mathiasr): Different behavior CfSourceCode vs JarSourceCode")
+  @Test
+  public void testRegress70737019() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("regress_70737019", "Test");
+  }
+
+  @Test
+  public void testRegress72361252() throws Exception {
+    // D8 output has variable with empty name.
+    testDebuggingJvmOnly("regress_72361252", "Test");
+  }
+
+  @Test
+  public void testMemberrebinding2() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("memberrebinding2", "Memberrebinding");
+  }
+
+  @Test
+  public void testMemberrebinding3() throws Exception {
+    testDebugging("memberrebinding3", "Memberrebinding");
+  }
+
+  @Test
+  public void testMinification() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("minification", "Minification");
+  }
+
+  @Test
+  public void testEnclosingmethod() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("enclosingmethod", "Main");
+  }
+
+  @Test
+  public void testEnclosingmethod_proguarded() throws Exception {
+    // TODO(b/79671093): We don't match JVM's behavior on this example.
+    testDebuggingJvmOutputOnly("enclosingmethod_proguarded", "Main");
+  }
+
+  @Test
+  public void testInterfaceinlining() throws Exception {
+    testDebugging("interfaceinlining", "Main");
+  }
+
+  @Test
+  public void testSwitchmaps() throws Exception {
+    // TODO(b/79671093): D8 has different line number info during stepping.
+    testDebuggingJvmOnly("switchmaps", "Switches");
+  }
+
+  private void testDebugging(String pkg, String clazz) throws Exception {
+    init(pkg, clazz)
+        .add("Input", input())
+        .add("R8/CfSourceCode", r8cf())
+        .add("R8/JarSourceCode", r8jar())
+        .add("D8", d8())
+        .compare();
+  }
+
+  private void testDebuggingJvmOnly(String pkg, String clazz) throws Exception {
+    init(pkg, clazz)
+        .add("Input", input())
+        .add("R8/CfSourceCode", r8cf())
+        .add("R8/JarSourceCode", r8jar())
+        .compare();
+  }
+
+  private void testDebuggingJvmOutputOnly(String pkg, String clazz) throws Exception {
+    init(pkg, clazz)
+        .add("R8/CfSourceCode", r8cf())
+        .add("R8/JarSourceCode", r8jar())
+        .compare();
+  }
+
+  private DebugStreamComparator init(String pkg, String clazz) throws Exception {
     // See verifyStateLocation in DebugTestBase.
-    Assume.assumeTrue("Streaming on Dalvik DEX runtimes has some unknown interference issue",
+    Assume.assumeTrue(
+        "Streaming on Dalvik DEX runtimes has some unknown interference issue",
         ToolHelper.getDexVm().getVersion().isAtLeast(Version.V6_0_1));
-    Assume.assumeTrue("Skipping test " + testName.getMethodName()
+    Assume.assumeTrue(
+        "Skipping test "
+            + testName.getMethodName()
             + " because debug tests are not yet supported on Windows",
         !ToolHelper.isWindows());
-    String pkg = "arithmetic";
-    String clazzName = pkg + ".Arithmetic";
-    Stream<DebuggeeState> cf = streamDebugTest(cfConfig("arithmetic"), clazzName, ANDROID_FILTER);
-    Stream<DebuggeeState> d8 = streamDebugTest(d8Config("arithmetic"), clazzName, ANDROID_FILTER);
-    Stream<DebuggeeState> r8 =
-        streamDebugTest(r8DebugCfConfig("arithmetic"), clazzName, ANDROID_FILTER);
-    new DebugStreamComparator()
-        .add("CF", cf)
-        .add("D8", d8)
-        .add("R8/CF", r8)
-        .compare();
+    clazzName = pkg + "." + clazz;
+    inputJar =
+        Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, pkg + "_debuginfo_all" + FileUtils.JAR_EXTENSION);
+    return new DebugStreamComparator();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
index 843a0f8..bc390b9 100644
--- a/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
@@ -119,12 +119,6 @@
     } else {
       builder.addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()));
     }
-    // TODO(b/75997473): Enable inlining when supported by CF backend
-    ToolHelper.runR8(
-        builder.build(),
-        options -> {
-          options.enableInlining = false;
-          options.invalidDebugInfoFatal = true;
-        });
+    ToolHelper.runR8(builder.build(), options -> options.invalidDebugInfoFatal = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
index 5ddd98c..63bc007 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinDebugTestBase.java
@@ -4,9 +4,6 @@
 
 package com.android.tools.r8.debug;
 
-import com.android.tools.r8.ToolHelper;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.List;
 import org.apache.harmony.jpda.tests.framework.jdwp.Frame.Variable;
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
index cdba21b..3837bb1 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinInlineTest.java
@@ -6,40 +6,46 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.apache.harmony.jpda.tests.framework.jdwp.Value;
 import org.junit.Test;
 
-// TODO check double-depth inline (an inline in another inline)
 public class KotlinInlineTest extends KotlinDebugTestBase {
 
+  public static final String DEBUGGEE_CLASS = "KotlinInline";
+  public static final String SOURCE_FILE = "KotlinInline.kt";
+
   @Test
   public void testStepOverInline() throws Throwable {
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         kotlinStepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(43, s.getLineNumber());
           s.checkLocal("this");
         }),
@@ -51,29 +57,29 @@
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepInto(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           // The actual line number (the one encoded in debug information) is different than the
           // source file one.
           // TODO(shertz) extract original line number from JSR-45's SMAP (only supported on
@@ -89,47 +95,65 @@
     String methodName = "singleInline";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", methodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, methodName),
         run(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(41, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepOver(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(42, s.getLineNumber());
           s.checkLocal("this");
         }),
         stepInto(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
         }),
         kotlinStepOut(),
         inspect(s -> {
-          assertEquals("KotlinInline", s.getClassName());
+          assertEquals(DEBUGGEE_CLASS, s.getClassName());
           assertEquals(methodName, s.getMethodName());
-          assertEquals("KotlinInline.kt", s.getSourceFile());
+          assertEquals(SOURCE_FILE, s.getSourceFile());
           assertEquals(43, s.getLineNumber());
           s.checkLocal("this");
         }),
         run());
   }
 
+  private static String mangleFunctionNameFromInlineScope(String functionName) {
+    return "$i$f$" + functionName;
+  }
+
+  private static String mangleLambdaNameFromInlineScope(String functionName, int lambdaId) {
+    assert lambdaId > 0;
+    return "$i$a$" + lambdaId + "$" + functionName;
+  }
+
+  private static String mangleLvNameFromInlineScope(String lvName, int inlineDepth) {
+    assert inlineDepth > 0;
+    StringBuilder builder = new StringBuilder(lvName);
+    for (int i = 0; i < inlineDepth; ++i) {
+      builder.append("$iv");
+    }
+    return builder.toString();
+  }
+
   @Test
   public void testKotlinInline() throws Throwable {
     final String inliningMethodName = "invokeInlinedFunctions";
     runDebugTest(
         getD8Config(),
-        "KotlinInline",
-        breakpoint("KotlinInline", inliningMethodName),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, inliningMethodName),
         run(),
         inspect(s -> {
           assertEquals(inliningMethodName, s.getMethodName());
@@ -157,8 +181,8 @@
           s.checkLocal("this");
           s.checkLocal("inA", Value.createInt(1));
           // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedA");
-          s.checkLocal("$i$a$1$inlinedA");
+          s.checkLocal(mangleFunctionNameFromInlineScope("inlinedA"));
+          s.checkLocal(mangleLambdaNameFromInlineScope("inlinedA", 1));
         }),
         stepInto(),
         inspect(s -> {
@@ -181,10 +205,144 @@
           s.checkLocal("this");
           s.checkLocal("inB", Value.createInt(2));
           // This is a "hidden" lv added by Kotlin (which is neither initialized nor used).
-          s.checkLocal("$i$f$inlinedB");
-          s.checkLocal("$i$a$1$inlinedB");
+          s.checkLocal(mangleFunctionNameFromInlineScope("inlinedB"));
+          s.checkLocal(mangleLambdaNameFromInlineScope("inlinedB", 1));
         }),
         run());
   }
 
+  @Test
+  public void testNestedInlining() throws Throwable {
+    // Count the number of lines in the source file. This is needed to check that inlined code
+    // refers to non-existing line numbers.
+    Path sourceFilePath = Paths.get(ToolHelper.TESTS_DIR, "debugTestResourcesKotlin", SOURCE_FILE);
+    assert sourceFilePath.toFile().exists();
+    final int maxLineNumber = Files.readAllLines(sourceFilePath).size();
+    final String inliningMethodName = "testNestedInlining";
+
+    // Local variables that represent the scope (start,end) of function's code that has been
+    // inlined.
+    final String inlinee1_inlineScope = mangleFunctionNameFromInlineScope("inlinee1");
+    final String inlinee2_inlineScope = mangleFunctionNameFromInlineScope("inlinee2");
+
+    // Local variables that represent the scope (start,end) of lambda's code that has been inlined.
+    final String inlinee2_lambda1_inlineScope = mangleLambdaNameFromInlineScope("inlinee2", 1);
+    final String inlinee2_lambda2_inlineScope = mangleLambdaNameFromInlineScope("inlinee2", 2);
+    final String c_mangledLvName = mangleLvNameFromInlineScope("c", 1);
+    final String left_mangledLvName = mangleLvNameFromInlineScope("left", 1);
+    final String right_mangledLvName = mangleLvNameFromInlineScope("right", 1);
+    final String p_mangledLvName = mangleLvNameFromInlineScope("p", 2);
+
+    runDebugTest(
+        getD8Config(),
+        DEBUGGEE_CLASS,
+        breakpoint(DEBUGGEE_CLASS, inliningMethodName),
+        run(),
+        inspect(s -> {
+          assertEquals(inliningMethodName, s.getMethodName());
+          assertEquals(52, s.getLineNumber());
+          s.checkLocal("this");
+        }),
+        checkLocal("this"),
+        checkNoLocals("l1", "l2"),
+        stepOver(),
+        checkLine(SOURCE_FILE, 53),
+        checkLocals("this", "l1"),
+        checkNoLocal("l2"),
+        stepOver(),
+        checkLine(SOURCE_FILE, 54),
+        checkLocals("this", "l1", "l2"),
+        stepInto(),
+        // We jumped into 1st inlinee but the current method is the same
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        checkNoLocal(c_mangledLvName),
+        stepInto(),
+        checkLocal(c_mangledLvName),
+        stepInto(),
+        // We jumped into 2nd inlinee which is nested in the 1st inlinee
+        checkLocal(inlinee2_inlineScope),
+        checkLocal(inlinee1_inlineScope),
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        // We must see the local variable "p" with a 2-level inline depth.
+        checkLocal(p_mangledLvName),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        // Enter the if block of inlinee2
+        stepInto(),
+        checkLocal(p_mangledLvName),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        // Enter the inlined lambda
+        stepInto(),
+        checkLocal(p_mangledLvName),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocals(left_mangledLvName, right_mangledLvName),
+        stepInto(),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkLocal(left_mangledLvName),
+        checkNoLocal(right_mangledLvName),
+        stepInto(),
+        checkLocals(left_mangledLvName, right_mangledLvName),
+        // Enter "foo"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, "foo"),
+        checkLine(SOURCE_FILE, 34),
+        stepOut(),
+        // We're back to the inline section, at the end of the lambda
+        inspect(state -> {
+          assertEquals(SOURCE_FILE, state.getSourceFile());
+          assertTrue(state.getLineNumber() > maxLineNumber);
+        }),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        stepInto(),
+        // We're in inlinee2, after the call to the inlined lambda.
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        checkLocal(p_mangledLvName),
+        stepInto(),
+        // We're out of inlinee2
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkNoLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        // Enter the new call to "inlinee2"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        checkNoLocal(p_mangledLvName),
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkNoLocal(inlinee2_lambda2_inlineScope),
+        checkNoLocal(p_mangledLvName),
+        // We enter the 2nd lambda
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, inliningMethodName),
+        checkLocal(inlinee1_inlineScope),
+        checkLocal(inlinee2_inlineScope),
+        checkNoLocal(inlinee2_lambda1_inlineScope),
+        checkLocal(inlinee2_lambda2_inlineScope),
+        // Enter the call to "foo"
+        stepInto(),
+        checkMethod(DEBUGGEE_CLASS, "foo"),
+        checkLine(SOURCE_FILE, 34),
+        run());
+  }
+
 }
diff --git a/src/test/java/com/android/tools/r8/debug/NopDebugTestRunner.java b/src/test/java/com/android/tools/r8/debug/NopDebugTestRunner.java
new file mode 100644
index 0000000..3ea4017
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/NopDebugTestRunner.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import org.junit.Assume;
+import org.junit.Test;
+
+public class NopDebugTestRunner extends DebugTestBase {
+
+  private final Class<?> CLAZZ = NopTest.class;
+
+  @Test
+  public void testNop() throws Exception {
+    // R8 will dead-code eliminate "args = args" in NopTest.
+    // In debug mode the programmer should still be able to break on dead code,
+    // so R8 inserts a NOP in the output. Test that CfNop.buildIR() works.
+    runTwiceTest(writeInput(CLAZZ), CLAZZ.getName());
+  }
+
+  private Path writeInput(Class<?> clazz) throws Exception {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(inputJar);
+    String descriptor = DescriptorUtils.javaTypeToDescriptor(clazz.getName());
+    inputConsumer.accept(ToolHelper.getClassAsBytes(clazz), descriptor, null);
+    inputConsumer.finished(null);
+    return inputJar;
+  }
+
+  private void runTwiceTest(Path inputJar, String mainClass) throws Exception {
+    Path output1 = runR8(inputJar, "output1.jar");
+    Path output2 = runR8(output1, "output2.jar");
+    stepOutput(mainClass, inputJar, output1, output2);
+  }
+
+  private Path runR8(Path inputJar, String outputName) throws Exception {
+    Path outputJar = temp.getRoot().toPath().resolve(outputName);
+    ToolHelper.runR8(
+        R8Command.builder()
+            .addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME))
+            .setMode(CompilationMode.DEBUG)
+            .addProgramFiles(inputJar)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .build(),
+        o -> o.enableCfFrontend = true);
+    return outputJar;
+  }
+
+  private void stepOutput(String mainClass, Path inputJar, Path output1, Path output2)
+      throws Exception {
+    Assume.assumeTrue(
+        "Skipping test "
+            + testName.getMethodName()
+            + " because debug tests are not yet supported on Windows",
+        !ToolHelper.isWindows());
+    new DebugStreamComparator()
+        .add("Input", streamDebugTest(mainClass, new CfDebugTestConfig(inputJar)))
+        .add("R8/CF", streamDebugTest(mainClass, new CfDebugTestConfig(output1)))
+        .add("R8/CF^2", streamDebugTest(mainClass, new CfDebugTestConfig(output2)))
+        .compare();
+  }
+
+  private Stream<DebuggeeState> streamDebugTest(String mainClass, DebugTestConfig config)
+      throws Exception {
+    return streamDebugTest(config, mainClass, ANDROID_FILTER);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/NopTest.java b/src/test/java/com/android/tools/r8/debug/NopTest.java
new file mode 100644
index 0000000..8d51961
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/NopTest.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+public class NopTest {
+
+  public static void main(String[] args) {
+    args = args;
+    System.out.println(args);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTest.java b/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTest.java
new file mode 100644
index 0000000..8fd6cd8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTest.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+public class UnusedCheckCastTargetOptimizationTest {
+
+  class Super {}
+
+  class Subclass extends Super {}
+
+  public static void main(String[] args) {
+    Super[] b = new Subclass[10];
+    Subclass[] c = (Subclass[]) b;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTestRunner.java b/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTestRunner.java
new file mode 100644
index 0000000..2b93218
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/UnusedCheckCastTargetOptimizationTestRunner.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+import org.junit.Test;
+
+public class UnusedCheckCastTargetOptimizationTestRunner extends DebugTestBase {
+
+  private static final Class MAIN_CLASS = UnusedCheckCastTargetOptimizationTest.class;
+  private static final Class SUPER_CLASS = UnusedCheckCastTargetOptimizationTest.Super.class;
+  private static final Class SUBCLASS_CLASS = UnusedCheckCastTargetOptimizationTest.Subclass.class;
+  private static final String FILE = MAIN_CLASS.getSimpleName() + ".java";
+  private static final String NAME = MAIN_CLASS.getCanonicalName();
+
+  @Test
+  public void test() throws Throwable {
+    runDebugTest(
+        new D8DebugTestConfig().compileAndAddClasses(temp, MAIN_CLASS, SUPER_CLASS, SUBCLASS_CLASS),
+        NAME,
+        breakpoint(NAME, "main", 14),
+        run(),
+        checkLine(FILE, 14),
+        checkLocal("b"),
+        checkNoLocal("c"),
+        stepOver(),
+        checkLine(FILE, 15),
+        checkLocal("b"),
+        checkLocal("c"),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
index 108b474..103c07f 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
@@ -78,12 +78,6 @@
     } else {
       builder.addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()));
     }
-    // TODO(b/75997473): Enable inlining when supported by CF backend
-    ToolHelper.runR8(
-        builder.build(),
-        options -> {
-          options.enableInlining = false;
-          options.invalidDebugInfoFatal = true;
-        });
+    ToolHelper.runR8(builder.build(), options -> options.invalidDebugInfoFatal = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java b/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
index 9edc735..89935a7 100644
--- a/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
+++ b/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexCode.Try;
@@ -25,6 +24,7 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
@@ -80,7 +80,7 @@
             holder, dexItemFactory.createProto(dexItemFactory.voidType), "theMethod"),
         MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC, false),
         DexAnnotationSet.empty(),
-        DexAnnotationSetRefList.empty(),
+        ParameterAnnotationsList.empty(),
         code);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/PhiDefinitionsTestRunner.java b/src/test/java/com/android/tools/r8/ir/PhiDefinitionsTestRunner.java
index f9541e5..4f2bdeb 100644
--- a/src/test/java/com/android/tools/r8/ir/PhiDefinitionsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/ir/PhiDefinitionsTestRunner.java
@@ -115,12 +115,6 @@
     } else {
       builder.addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()));
     }
-    // TODO(b/75997473): Enable inlining when supported by CF backend
-    ToolHelper.runR8(
-        builder.build(),
-        options -> {
-          options.enableInlining = false;
-          options.invalidDebugInfoFatal = true;
-        });
+    ToolHelper.runR8(builder.build(), options -> options.invalidDebugInfoFatal = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
index 5150223..16785f6 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.ir.optimize.nonnull.NonNullAfterInvoke;
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexInspector;
@@ -57,7 +58,7 @@
     AppInfo appInfo = new AppInfo(dexApplication);
     DexInspector dexInspector = new DexInspector(appInfo.app);
     DexEncodedMethod foo = dexInspector.clazz(mainClass.getName()).method(signature).getMethod();
-    IRCode irCode = foo.buildIR(TEST_OPTIONS);
+    IRCode irCode = foo.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
     NonNullTracker nonNullTracker = new NonNullTracker();
     nonNullTracker.addNonNull(irCode);
     TypeAnalysis analysis = new TypeAnalysis(appInfo, foo, irCode);
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
index ed15f7c..967b33e 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
@@ -125,7 +125,7 @@
                 new MethodSignature("subtractConstants8bitRegisters", "int", ImmutableList.of()))
             .getMethod();
     try {
-      IRCode irCode = subtract.buildIR(TEST_OPTIONS);
+      IRCode irCode = subtract.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, subtract, irCode);
       analysis.forEach((v, l) -> {
         assertEither(l, PRIMITIVE, NULL, TOP);
@@ -143,7 +143,7 @@
             .method(new MethodSignature("fibonacci", "int", ImmutableList.of("int")))
             .getMethod();
     try {
-      IRCode irCode = fib.buildIR(TEST_OPTIONS);
+      IRCode irCode = fib.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, fib, irCode);
       analysis.forEach((v, l) -> {
         assertEither(l, PRIMITIVE, NULL);
@@ -161,7 +161,7 @@
             .method(new MethodSignature("test1", "int[]", ImmutableList.of()))
             .getMethod();
     try {
-      IRCode irCode = test1.buildIR(TEST_OPTIONS);
+      IRCode irCode = test1.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, test1, irCode);
       Value array = null;
       InstructionIterator iterator = irCode.instructionIterator();
@@ -196,7 +196,7 @@
             .method(new MethodSignature("test4", "int[]", ImmutableList.of()))
             .getMethod();
     try {
-      IRCode irCode = test4.buildIR(TEST_OPTIONS);
+      IRCode irCode = test4.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, test4, irCode);
       Value array = null;
       InstructionIterator iterator = irCode.instructionIterator();
@@ -231,7 +231,7 @@
             .method(new MethodSignature("loop2", "void", ImmutableList.of()))
             .getMethod();
     try {
-      IRCode irCode = loop2.buildIR(TEST_OPTIONS);
+      IRCode irCode = loop2.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, loop2, irCode);
       analysis.forEach((v, l) -> {
         if (l.isClassTypeLatticeElement()) {
@@ -254,7 +254,7 @@
             .method(new MethodSignature("test2_throw", "int", ImmutableList.of()))
             .getMethod();
     try {
-      IRCode irCode = test2.buildIR(TEST_OPTIONS);
+      IRCode irCode = test2.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, test2, irCode);
       analysis.forEach((v, l) -> {
         if (l.isClassTypeLatticeElement()) {
@@ -284,7 +284,7 @@
         CheckCast.class, new ClassTypeLatticeElement(test, true),
         NewInstance.class, new ClassTypeLatticeElement(test, false));
     try {
-      IRCode irCode = method.buildIR(TEST_OPTIONS);
+      IRCode irCode = method.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, method, irCode);
       analysis.forEach((v, l) -> verifyTypeEnvironment(expectedLattices, v, l));
     } catch (ApiLevelException e) {
@@ -306,7 +306,7 @@
       InstanceOf.class, PRIMITIVE,
       StaticGet.class, new ClassTypeLatticeElement(test, true));
     try {
-      IRCode irCode = method.buildIR(TEST_OPTIONS);
+      IRCode irCode = method.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
       TypeAnalysis analysis = new TypeAnalysis(appInfo, method, irCode);
       analysis.forEach((v, l) -> verifyTypeEnvironment(expectedLattices, v, l));
     } catch (ApiLevelException e) {
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/A.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/A.java
new file mode 100644
index 0000000..c700163
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/A.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+public class A {
+  public A method() {
+    return new A();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/B.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/B.java
new file mode 100644
index 0000000..7d8134f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/B.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+public class B extends A {
+  @Override
+  public A method() {
+    return new B();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/C.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/C.java
new file mode 100644
index 0000000..90030d5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/C.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+public class C extends B {
+  @Override
+  public A method() {
+    return new C();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/Client.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/Client.java
new file mode 100644
index 0000000..0c6bd44
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/Client.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+public class Client {
+  public static void main(String[] args) {
+    A a = new A().method();
+    A b = new B().method();
+    A c = new C().method();
+
+    System.out.println("a=" + a.getClass().getSimpleName());
+    System.out.println("b=" + b.getClass().getSimpleName());
+    System.out.println("c=" + c.getClass().getSimpleName());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnType.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnType.java
new file mode 100644
index 0000000..3d7a7fa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnType.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is a copy of dalvik.annotation.codegen.CovariantReturnType.
+@Repeatable(CovariantReturnType.CovariantReturnTypes.class)
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD})
+public @interface CovariantReturnType {
+  Class<?> returnType();
+
+  int presentAfter();
+
+  @Retention(RetentionPolicy.CLASS)
+  @Target({ElementType.METHOD})
+  @interface CovariantReturnTypes {
+    CovariantReturnType[] value();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnTypeAnnotationTransformerTest.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnTypeAnnotationTransformerTest.java
new file mode 100644
index 0000000..9c58d25
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/CovariantReturnTypeAnnotationTransformerTest.java
@@ -0,0 +1,191 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import java.util.Collections;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class CovariantReturnTypeAnnotationTransformerTest extends AsmTestBase {
+  public static final String PACKAGE_NAME = "com/android/tools/r8/ir/desugar/annotations";
+  public static final String CRT_NAME = "dalvik/annotation/codegen/CovariantReturnType";
+  public static final String CRTS_SIMPLE_NAME = "CovariantReturnTypes";
+
+  @Test
+  public void testVersion1WithClient1And2() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            ToolHelper.getClassAsBytes(Client.class),
+            ToolHelper.getClassAsBytes(A.class),
+            ToolHelper.getClassAsBytes(B.class),
+            ToolHelper.getClassAsBytes(C.class));
+
+    // Version 1 of the library should always work.
+    succeedsIndependentOfFlag(input, false);
+  }
+
+  @Test
+  public void testVersion1WithClient3() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            com.android.tools.r8.ir.desugar.annotations.version3.ClientDump.dump(),
+            ToolHelper.getClassAsBytes(A.class),
+            ToolHelper.getClassAsBytes(B.class),
+            ToolHelper.getClassAsBytes(C.class));
+
+    // There will be no methods with the signature "L.../B;->method()L.../B;" and
+    // "L.../C;->method()L.../C;".
+    failsIndependentOfFlag(input);
+  }
+
+  @Ignore("b/78618808")
+  @Test
+  public void testVersion2WithClient1And2() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            ToolHelper.getClassAsBytes(Client.class),
+            ToolHelper.getClassAsBytes(A.class),
+            com.android.tools.r8.ir.desugar.annotations.version2.BDump.dump(),
+            com.android.tools.r8.ir.desugar.annotations.version2.CDump.dump());
+
+    // Version 2 of the library should always work.
+    succeedsIndependentOfFlag(input, true);
+  }
+
+  @Ignore("b/78618808")
+  @Test
+  public void testVersion2WithClient3() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            com.android.tools.r8.ir.desugar.annotations.version3.ClientDump.dump(),
+            ToolHelper.getClassAsBytes(A.class),
+            com.android.tools.r8.ir.desugar.annotations.version2.BDump.dump(),
+            com.android.tools.r8.ir.desugar.annotations.version2.CDump.dump());
+
+    // If CovariantReturnType annotations are processed, then synthetic methods with the signatures
+    // "L.../B;->method()L.../B;" and "L.../C;->method()L.../C;" will be added by D8.
+    succeedsWithOption(input, true, true);
+
+    // If CovariantReturnType annotations are ignored, then there will be no methods with the
+    // signatures "L.../B;->method()L.../B;" and "L.../C;->method()L.../C;".
+    failsWithOption(input, false);
+  }
+
+  @Test
+  public void testVersion3WithClient3() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            com.android.tools.r8.ir.desugar.annotations.version3.ClientDump.dump(),
+            ToolHelper.getClassAsBytes(A.class),
+            com.android.tools.r8.ir.desugar.annotations.version3.BDump.dump(),
+            com.android.tools.r8.ir.desugar.annotations.version3.CDump.dump());
+
+    // Version 3 of the library should always work.
+    succeedsIndependentOfFlag(input, false);
+  }
+
+  @Test
+  public void testVersion3WithClient1And2() throws Exception {
+    AndroidApp input =
+        buildAndroidApp(
+            ToolHelper.getClassAsBytes(Client.class),
+            ToolHelper.getClassAsBytes(A.class),
+            com.android.tools.r8.ir.desugar.annotations.version3.BDump.dump(),
+            com.android.tools.r8.ir.desugar.annotations.version3.CDump.dump());
+
+    // Version 3 of the library should always work with client 1.
+    succeedsIndependentOfFlag(input, false);
+  }
+
+  private void succeedsWithOption(
+      AndroidApp input, boolean option, boolean checkPresenceOfSyntheticMethods) throws Exception {
+    AndroidApp output =
+        compileWithD8(input, options -> options.processCovariantReturnTypeAnnotations = option);
+    String stdout = runOnArt(output, Client.class.getCanonicalName());
+    Assert.assertEquals(getExpectedOutput(), stdout);
+
+    if (option && checkPresenceOfSyntheticMethods) {
+      checkPresenceOfSyntheticMethods(output);
+    }
+  }
+
+  private void failsWithOption(AndroidApp input, boolean option) throws Exception {
+    AndroidApp output =
+        compileWithD8(input, options -> options.processCovariantReturnTypeAnnotations = option);
+    ToolHelper.ProcessResult result = runOnArtRaw(output, Client.class.getCanonicalName());
+    assertThat(result.stderr, containsString("java.lang.NoSuchMethodError"));
+  }
+
+  private void succeedsIndependentOfFlag(AndroidApp input, boolean checkPresenceOfSyntheticMethods)
+      throws Exception {
+    succeedsWithOption(input, true, checkPresenceOfSyntheticMethods);
+    succeedsWithOption(input, false, checkPresenceOfSyntheticMethods);
+  }
+
+  private void failsIndependentOfFlag(AndroidApp input) throws Exception {
+    failsWithOption(input, true);
+    failsWithOption(input, false);
+  }
+
+  private void checkPresenceOfSyntheticMethods(AndroidApp output) throws Exception {
+    DexInspector inspector = new DexInspector(output);
+
+    // Get classes A, B, and C.
+    DexInspector.ClassSubject clazzA = inspector.clazz(A.class.getCanonicalName());
+    assertThat(clazzA, isPresent());
+
+    DexInspector.ClassSubject clazzB = inspector.clazz(B.class.getCanonicalName());
+    assertThat(clazzB, isPresent());
+
+    DexInspector.ClassSubject clazzC = inspector.clazz(C.class.getCanonicalName());
+    assertThat(clazzC, isPresent());
+
+    // Check that the original methods are there, and that they are not synthetic.
+    DexInspector.MethodSubject methodA =
+        clazzB.method(A.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodA, isPresent());
+    Assert.assertTrue(!methodA.getMethod().isSyntheticMethod());
+
+    DexInspector.MethodSubject methodB =
+        clazzB.method(A.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodB, isPresent());
+    Assert.assertTrue(!methodB.getMethod().isSyntheticMethod());
+
+    DexInspector.MethodSubject methodC =
+        clazzC.method(A.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodC, isPresent());
+    Assert.assertTrue(!methodC.getMethod().isSyntheticMethod());
+
+    // Check that a synthetic method has been added to class B.
+    DexInspector.MethodSubject methodB2 =
+        clazzB.method(B.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodB2, isPresent());
+    Assert.assertTrue(methodB2.getMethod().isSyntheticMethod());
+
+    // Check that two synthetic methods have been added to class C.
+    DexInspector.MethodSubject methodC2 =
+        clazzC.method(B.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodC2, isPresent());
+    Assert.assertTrue(methodC2.getMethod().isSyntheticMethod());
+
+    DexInspector.MethodSubject methodC3 =
+        clazzC.method(C.class.getCanonicalName(), "method", Collections.emptyList());
+    assertThat(methodC3, isPresent());
+    Assert.assertTrue(methodC3.getMethod().isSyntheticMethod());
+  }
+
+  private String getExpectedOutput() {
+    return "a=A\nb=B\nc=C\n";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/B.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/B.java
new file mode 100644
index 0000000..342e3d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/B.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version2;
+
+import com.android.tools.r8.ir.desugar.annotations.A;
+import com.android.tools.r8.ir.desugar.annotations.CovariantReturnType;
+
+public class B extends A {
+  @CovariantReturnType(returnType = B.class, presentAfter = 25)
+  @Override
+  public A method() {
+    return new B();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/BDump.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/BDump.java
new file mode 100644
index 0000000..3dadc2b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/BDump.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version2;
+
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.CRT_NAME;
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.PACKAGE_NAME;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// Generated by running tools/asmifier.py on build/classes/test/com/android/tools/r8/ir/desugar/
+// annotations/version2/B.class, removing the subpackage "version2" from all class names, and
+// manually changing the name of the CovariantReturnType annotation to
+// "Ldalvik/annotation/codegen/CovariantReturnType;".
+public class BDump implements Opcodes {
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, PACKAGE_NAME + "/B", null, PACKAGE_NAME + "/A", null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/A", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "method", "()L" + PACKAGE_NAME + "/A;", null, null);
+      {
+        av0 = mv.visitAnnotation("L" + CRT_NAME + ";", false);
+        av0.visit("returnType", Type.getType("L" + PACKAGE_NAME + "/B;"));
+        av0.visit("presentAfter", new Integer(25));
+        av0.visitEnd();
+      }
+      mv.visitCode();
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/B");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/B", "<init>", "()V", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/C.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/C.java
new file mode 100644
index 0000000..52fe17f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/C.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version2;
+
+import com.android.tools.r8.ir.desugar.annotations.A;
+import com.android.tools.r8.ir.desugar.annotations.CovariantReturnType;
+
+public class C extends B {
+  @CovariantReturnType(returnType = B.class, presentAfter = 25)
+  @CovariantReturnType(returnType = C.class, presentAfter = 28)
+  @Override
+  public A method() {
+    return new C();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/CDump.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/CDump.java
new file mode 100644
index 0000000..bc06250
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version2/CDump.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version2;
+
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.*;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// Generated by running tools/asmifier.py on build/classes/test/com/android/tools/r8/ir/desugar/
+// annotations/version2/C.class, removing the subpackage "version2" from all class names, and
+// manually changing the name of the CovariantReturnType and CovariantReturnTypes annotations to
+// "Ldalvik/annotation/codegen/CovariantReturnType;" and
+// "Ldalvik/annotation/codegen/CovariantReturnType$CovariantReturnTypes;", respectively.
+public class CDump implements Opcodes {
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, PACKAGE_NAME + "/C", null, PACKAGE_NAME + "/B", null);
+
+    cw.visitInnerClass(
+        CRT_NAME + "$" + CRTS_SIMPLE_NAME,
+        CRT_NAME,
+        CRTS_SIMPLE_NAME,
+        ACC_PUBLIC + ACC_STATIC + ACC_ANNOTATION + ACC_ABSTRACT + ACC_INTERFACE);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/B", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "method", "()L" + PACKAGE_NAME + "/A;", null, null);
+      {
+        av0 = mv.visitAnnotation("L" + CRT_NAME + "$" + CRTS_SIMPLE_NAME + ";", false);
+        {
+          AnnotationVisitor av1 = av0.visitArray("value");
+          {
+            AnnotationVisitor av2 = av1.visitAnnotation(null, "L" + CRT_NAME + ";");
+            av2.visit("returnType", Type.getType("L" + PACKAGE_NAME + "/B;"));
+            av2.visit("presentAfter", new Integer(25));
+            av2.visitEnd();
+          }
+          {
+            AnnotationVisitor av2 = av1.visitAnnotation(null, "L" + CRT_NAME + ";");
+            av2.visit("returnType", Type.getType("L" + PACKAGE_NAME + "/C;"));
+            av2.visit("presentAfter", new Integer(28));
+            av2.visitEnd();
+          }
+          av1.visitEnd();
+        }
+        av0.visitEnd();
+      }
+      mv.visitCode();
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/C");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/C", "<init>", "()V", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/B.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/B.java
new file mode 100644
index 0000000..1b1fa1c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/B.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+import com.android.tools.r8.ir.desugar.annotations.A;
+
+public class B extends A {
+  @Override
+  public B method() {
+    return new B();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/BDump.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/BDump.java
new file mode 100644
index 0000000..91c16c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/BDump.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.PACKAGE_NAME;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running tools/asmifier.py on build/classes/test/com/android/tools/r8/ir/desugar/
+// annotations/version3/B.class, and removing the subpackage "version3" from all class names.
+public class BDump implements Opcodes {
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "" + PACKAGE_NAME + "/B",
+        null,
+        "" + PACKAGE_NAME + "/A",
+        null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "" + PACKAGE_NAME + "/A", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "method", "()L" + PACKAGE_NAME + "/B;", null, null);
+      mv.visitCode();
+      mv.visitTypeInsn(NEW, "" + PACKAGE_NAME + "/B");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "" + PACKAGE_NAME + "/B", "<init>", "()V", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    {
+      mv =
+          cw.visitMethod(
+              ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC,
+              "method",
+              "()L" + PACKAGE_NAME + "/A;",
+              null,
+              null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "" + PACKAGE_NAME + "/B", "method", "()L" + PACKAGE_NAME + "/B;", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/C.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/C.java
new file mode 100644
index 0000000..fd3bedc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/C.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+public class C extends B {
+  @Override
+  public C method() {
+    return new C();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/CDump.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/CDump.java
new file mode 100644
index 0000000..219c3f1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/CDump.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.PACKAGE_NAME;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running tools/asmifier.py on build/classes/test/com/android/tools/r8/ir/desugar/
+// annotations/version3/C.class, and removing the subpackage "version3" from all class names.
+public class CDump implements Opcodes {
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, PACKAGE_NAME + "/C", null, PACKAGE_NAME + "/B", null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/B", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "method", "()L" + PACKAGE_NAME + "/C;", null, null);
+      mv.visitCode();
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/C");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/C", "<init>", "()V", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    {
+      mv =
+          cw.visitMethod(
+              ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC,
+              "method",
+              "()L" + PACKAGE_NAME + "/B;",
+              null,
+              null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, PACKAGE_NAME + "/C", "method", "()L" + PACKAGE_NAME + "/C;", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv =
+          cw.visitMethod(
+              ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC,
+              "method",
+              "()L" + PACKAGE_NAME + "/A;",
+              null,
+              null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, PACKAGE_NAME + "/C", "method", "()L" + PACKAGE_NAME + "/C;", false);
+      mv.visitInsn(ARETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/Client.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/Client.java
new file mode 100644
index 0000000..f2feffa
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/Client.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+import com.android.tools.r8.ir.desugar.annotations.A;
+
+public class Client {
+  public static void main(String[] args) {
+    A a = new A().method();
+    B b = new B().method();
+    C c = new C().method();
+
+    System.out.println("a=" + a.getClass().getSimpleName());
+    System.out.println("b=" + b.getClass().getSimpleName());
+    System.out.println("c=" + c.getClass().getSimpleName());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/ClientDump.java b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/ClientDump.java
new file mode 100644
index 0000000..84971e4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/annotations/version3/ClientDump.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.annotations.version3;
+
+import static com.android.tools.r8.ir.desugar.annotations.CovariantReturnTypeAnnotationTransformerTest.PACKAGE_NAME;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running tools/asmifier.py on build/classes/test/com/android/tools/r8/ir/desugar/
+// annotations/version3/Client.class, and removing the subpackage "version3" from all class names.
+public class ClientDump implements Opcodes {
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(
+        V1_8, ACC_PUBLIC + ACC_SUPER, PACKAGE_NAME + "/Client", null, "java/lang/Object", null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      mv.visitCode();
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/A");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/A", "<init>", "()V", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, PACKAGE_NAME + "/A", "method", "()L" + PACKAGE_NAME + "/A;", false);
+      mv.visitVarInsn(ASTORE, 1);
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/B");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/B", "<init>", "()V", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, PACKAGE_NAME + "/B", "method", "()L" + PACKAGE_NAME + "/B;", false);
+      mv.visitVarInsn(ASTORE, 2);
+      mv.visitTypeInsn(NEW, PACKAGE_NAME + "/C");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, PACKAGE_NAME + "/C", "<init>", "()V", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, PACKAGE_NAME + "/C", "method", "()L" + PACKAGE_NAME + "/C;", false);
+      mv.visitVarInsn(ASTORE, 3);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+      mv.visitLdcInsn("a=");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitVarInsn(ALOAD, 1);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+      mv.visitLdcInsn("b=");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitVarInsn(ALOAD, 2);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+      mv.visitLdcInsn("c=");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitVarInsn(ALOAD, 3);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(3, 4);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
index 1f7c990..be7ee92 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
@@ -50,7 +50,7 @@
     List<String> pgConfigs = ImmutableList.of(
         "-keep class " + CLASS_NAME + " { *; }",
         "-dontshrink");
-    AndroidApp app = compileWithR8(builder, pgConfigs, null);
+    AndroidApp app = compileWithR8(builder, pgConfigs, o -> o.enableClassInlining = false);
 
     DexEncodedMethod method = getMethod(app, CLASS_NAME, main);
     assertNotNull(method);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
index e8f2a2a..47f0db3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/MemberValuePropagationTest.java
@@ -110,7 +110,7 @@
             .addProguardConfigurationFiles(proguardConfig)
             .setDisableMinification(true)
             .build(),
-        null);
+        o -> o.enableClassInlining = false);
     return dexOutputDir.resolve("classes.dex");
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
index b6109d4..7c90976 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.ir.optimize.nonnull.NonNullAfterInvoke;
 import com.android.tools.r8.ir.optimize.nonnull.NonNullAfterNullCheck;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.InternalOptions;
@@ -47,7 +48,7 @@
     AppInfo appInfo = new AppInfo(dexApplication);
     DexInspector dexInspector = new DexInspector(appInfo.app);
     DexEncodedMethod foo = dexInspector.clazz(testClass.getName()).method(signature).getMethod();
-    IRCode irCode = foo.buildIR(TEST_OPTIONS);
+    IRCode irCode = foo.buildIR(appInfo, TEST_OPTIONS, Origin.unknown());
     checkCountOfNonNull(irCode, 0);
 
     NonNullTracker nonNullTracker = new NonNullTracker();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
new file mode 100644
index 0000000..e9319d2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -0,0 +1,161 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.code.NewInstance;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceBA;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClass;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface1;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface1Impl;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields;
+import com.android.tools.r8.ir.optimize.classinliner.trivial.TrivialTestClass;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.Sets;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class ClassInlinerTest extends TestBase {
+  @Test
+  public void testTrivial() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(TrivialTestClass.class),
+        ToolHelper.getClassAsBytes(TrivialTestClass.Inner.class),
+        ToolHelper.getClassAsBytes(ReferencedFields.class),
+        ToolHelper.getClassAsBytes(EmptyClass.class),
+        ToolHelper.getClassAsBytes(EmptyClassWithInitializer.class),
+        ToolHelper.getClassAsBytes(Iface1.class),
+        ToolHelper.getClassAsBytes(Iface1Impl.class),
+        ToolHelper.getClassAsBytes(Iface2.class),
+        ToolHelper.getClassAsBytes(Iface2Impl.class),
+        ToolHelper.getClassAsBytes(CycleReferenceAB.class),
+        ToolHelper.getClassAsBytes(CycleReferenceBA.class),
+        ToolHelper.getClassAsBytes(ClassWithFinal.class)
+    };
+    String main = TrivialTestClass.class.getCanonicalName();
+    ProcessResult javaOutput = runOnJava(main, classes);
+    assertEquals(0, javaOutput.exitCode);
+
+    AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
+
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testInner"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testConstructorMapping1"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields"),
+        collectNewInstanceTypes(clazz, "testConstructorMapping2"));
+
+    assertEquals(
+        Collections.singleton("java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testConstructorMapping3"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testEmptyClass"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.EmptyClassWithInitializer"),
+        collectNewInstanceTypes(clazz, "testEmptyClassWithInitializer"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal"),
+        collectNewInstanceTypes(clazz, "testClassWithFinalizer"));
+
+    assertEquals(
+        Collections.emptySet(),
+        collectNewInstanceTypes(clazz, "testCallOnIface1"));
+
+    assertEquals(
+        Collections.singleton(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl"),
+        collectNewInstanceTypes(clazz, "testCallOnIface2"));
+
+    assertEquals(
+        Sets.newHashSet(
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB",
+            "java.lang.StringBuilder"),
+        collectNewInstanceTypes(clazz, "testCycles"));
+
+    assertEquals(
+        Sets.newHashSet("java.lang.StringBuilder",
+            "com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB"),
+        collectNewInstanceTypes(inspector.clazz(CycleReferenceAB.class), "foo", "int"));
+
+    assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
+  }
+
+  private Set<String> collectNewInstanceTypes(
+      ClassSubject clazz, String methodName, String... params) {
+    assertNotNull(clazz);
+    MethodSignature signature = new MethodSignature(methodName, "void", params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return filterInstructionKind(code, NewInstance.class)
+        .map(insn -> ((NewInstance) insn).getType().toSourceString())
+        .collect(Collectors.toSet());
+  }
+
+  private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
+    String config = keepMainProguardConfiguration(mainClass) + "\n"
+        + "-dontobfuscate\n"
+        + "-allowaccessmodification";
+
+    AndroidApp compiled = compileWithR8(app, config, o -> o.enableClassInlining = true);
+
+    // Materialize file for execution.
+    Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
+    compiled.writeToZip(generatedDexFile, OutputMode.DexIndexed);
+
+    // Run with ART.
+    String artOutput = ToolHelper.runArtNoVerificationErrors(
+        generatedDexFile.toString(), mainClass.getCanonicalName());
+
+    // Compare with Java.
+    ToolHelper.ProcessResult javaResult = ToolHelper.runJava(
+        ToolHelper.getClassPathForTests(), mainClass.getCanonicalName());
+
+    if (javaResult.exitCode != 0) {
+      System.out.println(javaResult.stdout);
+      System.err.println(javaResult.stderr);
+      fail("JVM failed for: " + mainClass);
+    }
+    assertEquals("JVM and ART output differ", javaResult.stdout, artOutput);
+
+    return compiled;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
new file mode 100644
index 0000000..7e0239c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ClassWithFinal.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class ClassWithFinal {
+  public String doNothing() {
+    return "nothing at all";
+  }
+
+  @Override
+  protected void finalize() throws Throwable {
+    doNothing();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
new file mode 100644
index 0000000..d26641a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class CycleReferenceAB {
+  private String a;
+
+  public CycleReferenceAB(String a) {
+    this.a = a;
+  }
+
+  public void foo(int depth) {
+    CycleReferenceBA ba = new CycleReferenceBA("depth=" + depth);
+    System.out.println("CycleReferenceAB::foo(" + depth + ")");
+    if (depth > 0) {
+      ba.foo(depth - 1);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CycleReferenceAB(" + a + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java
new file mode 100644
index 0000000..1c5d147
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceBA.java
@@ -0,0 +1,26 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class CycleReferenceBA {
+  private String a;
+
+  public CycleReferenceBA(String a) {
+    this.a = a;
+  }
+
+  public void foo(int depth) {
+    CycleReferenceAB ab = new CycleReferenceAB("depth=" + depth);
+    System.out.println("CycleReferenceBA::foo(" + depth + ")");
+    if (depth > 0) {
+      ab.foo(depth - 1);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CycleReferenceBA(" + a + ")";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java
new file mode 100644
index 0000000..cedfd77
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClass.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class EmptyClass {
+  public String doNothing() {
+    return "nothing at all";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java
new file mode 100644
index 0000000..9671ea5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/EmptyClassWithInitializer.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class EmptyClassWithInitializer {
+  public String doNothing() {
+    return "nothing at all";
+  }
+
+  static {
+    System.out.println("EmptyClassWithInitializer.<clinit>()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java
new file mode 100644
index 0000000..8320ec9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public interface Iface1 {
+  void foo();
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java
new file mode 100644
index 0000000..e42a04b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface1Impl.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class Iface1Impl implements Iface1 {
+  final String value;
+
+  public Iface1Impl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public void foo() {
+    System.out.println(value);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java
new file mode 100644
index 0000000..93a3d5d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public interface Iface2 extends Iface1 {
+  Integer CONSTANT = 123;
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java
new file mode 100644
index 0000000..17dd4c1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/Iface2Impl.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class Iface2Impl implements Iface2 {
+  final String value;
+
+  public Iface2Impl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public void foo() {
+    System.out.println(value);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java
new file mode 100644
index 0000000..90263d6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/ReferencedFields.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class ReferencedFields {
+  private String a;
+  private String b;
+
+  public ReferencedFields(String a, String b) {
+    this.a = a;
+    this.b = b;
+  }
+
+  public ReferencedFields(String a) {
+    this.a = a;
+  }
+
+  public String getConcat() {
+    return a + " " + b;
+  }
+
+  public String getA() {
+    return a;
+  }
+
+  public String getB() {
+    return b;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java
new file mode 100644
index 0000000..7e3cba2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/TrivialTestClass.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.classinliner.trivial;
+
+public class TrivialTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    TrivialTestClass test = new TrivialTestClass();
+    test.testInner();
+    test.testConstructorMapping1();
+    test.testConstructorMapping2();
+    test.testConstructorMapping3();
+    test.testEmptyClass();
+    test.testEmptyClassWithInitializer();
+    test.testClassWithFinalizer();
+    test.testCallOnIface1();
+    test.testCallOnIface2();
+    test.testCycles();
+  }
+
+  private synchronized void testInner() {
+    Inner inner = new Inner("inner{", 123, next() + "}");
+    System.out.println(inner.toString() + " " + inner.getPrefix() + " = " + inner.prefix);
+  }
+
+  private synchronized void testConstructorMapping1() {
+    ReferencedFields o = new ReferencedFields(next());
+    System.out.println(o.getA());
+  }
+
+  private synchronized void testConstructorMapping2() {
+    ReferencedFields o = new ReferencedFields(next());
+    System.out.println(o.getB());
+  }
+
+  private synchronized void testConstructorMapping3() {
+    ReferencedFields o = new ReferencedFields(next(), next());
+    System.out.println(o.getA() + o.getB() + o.getConcat());
+  }
+
+  private synchronized void testEmptyClass() {
+    new EmptyClass();
+  }
+
+  private synchronized void testEmptyClassWithInitializer() {
+    new EmptyClassWithInitializer();
+  }
+
+  private synchronized void testClassWithFinalizer() {
+    new ClassWithFinal();
+  }
+
+  private void callOnIface1(Iface1 iface) {
+    iface.foo();
+  }
+
+  private synchronized void testCallOnIface1() {
+    callOnIface1(new Iface1Impl(next()));
+  }
+
+  private void callOnIface2(Iface2 iface) {
+    iface.foo();
+  }
+
+  private synchronized void testCallOnIface2() {
+    callOnIface2(new Iface2Impl(next()));
+    System.out.println(Iface2Impl.CONSTANT); // Keep constant referenced
+  }
+
+  private synchronized void testCycles() {
+    new CycleReferenceAB("first").foo(3);
+    new CycleReferenceBA("second").foo(4);
+  }
+
+  public class Inner {
+    private String prefix;
+    private String suffix;
+    private double id;
+
+    public Inner(String prefix, double id, String suffix) {
+      this.prefix = prefix;
+      this.suffix = suffix;
+      this.id = id;
+    }
+
+    @Override
+    public String toString() {
+      return prefix + id + suffix;
+    }
+
+    public String getPrefix() {
+      return prefix;
+    }
+
+    public String getSuffix() {
+      return suffix;
+    }
+
+    public double getId() {
+      return id;
+    }
+
+    public void setPrefix(String prefix) {
+      this.prefix = prefix;
+    }
+
+    public void setSuffix(String suffix) {
+      this.suffix = suffix;
+    }
+
+    public void setId(double id) {
+      this.id = id;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/B79405526.java b/src/test/java/com/android/tools/r8/ir/regalloc/B79405526.java
index f03e6f6..08058e5 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/B79405526.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/B79405526.java
@@ -5,12 +5,9 @@
 package com.android.tools.r8.ir.regalloc;
 
 import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import org.junit.Test;
@@ -22,13 +19,13 @@
     DexInspector inspector = new DexInspector(app);
     DexInspector.ClassSubject clazz = inspector.clazz(TestClass.class);
     assertThat(clazz, isPresent());
-    // TODO(christofferqa): Ensure runOnArt checks that there are no verification errors, and then
-    // use runOnArt instead of runOnArtRaw.
-    ToolHelper.ProcessResult d8Result = runOnArtRaw(app, TestClass.class.getCanonicalName());
-    assertThat(d8Result.stderr, not(containsString("Verification error")));
+    // Throws if a method in TestClass does not verify.
+    runOnArt(app, TestClass.class.getName());
   }
 
   private static class TestClass {
+    public static void main(String[] args) {}
+
     public void method() {
       Object x = this;
       TestClass y = this;
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index 674fba8..b50718d 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -5,6 +5,8 @@
 
 import static com.android.tools.r8.utils.DescriptorUtils.getPathFromDescriptor;
 
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.naming.MemberNaming.FieldSignature;
@@ -136,7 +138,11 @@
       return addMethod("public", name, argumentTypes, returnType, lines);
     }
 
-    public MethodSignature addNativeMethod(
+    /**
+     * Note that the JVM rejects native methods with code. This method is used to test that D8
+     * removes code from native methods.
+     */
+    public MethodSignature addNativeMethodWithCode(
         String name,
         List<String> argumentTypes,
         String returnType,
@@ -367,6 +373,19 @@
     return outputs;
   }
 
+  public void writeClassFiles(ClassFileConsumer consumer, DiagnosticsHandler handler)
+      throws Exception {
+    for (ClassBuilder clazz : classes) {
+      consumer.accept(compile(clazz), clazz.getDescriptor(), handler);
+    }
+  }
+
+  public void writeJar(Path output, DiagnosticsHandler handler) throws Exception {
+    ClassFileConsumer consumer = new ClassFileConsumer.ArchiveConsumer(output);
+    writeClassFiles(consumer, handler);
+    consumer.finished(handler);
+  }
+
   public DexApplication read() throws Exception {
     return read(new InternalOptions());
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
index 5e41a25..ec18fac 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JumpSubroutineTests.java
@@ -8,11 +8,15 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassFileVersion;
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.Test;
 
 public class JumpSubroutineTests extends JasminTestBase {
@@ -22,10 +26,28 @@
     assertEquals(expected, javaResult);
     String artResult = runOnArtD8(builder, main);
     assertEquals(expected, artResult);
+    String cfFrontendResult = runOnJavaR8CfFrontend(builder, main);
+    assertEquals(expected, cfFrontendResult);
     String dxArtResult = runOnArtDx(builder, main);
     assertEquals(expected, dxArtResult);
   }
 
+  private String runOnJavaR8CfFrontend(JasminBuilder builder, String main) throws Exception {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    Path outputJar = temp.getRoot().toPath().resolve("output.jar");
+    builder.writeJar(inputJar, null);
+    ToolHelper.runR8(
+        R8Command.builder()
+            .addProgramFiles(inputJar)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME))
+            .build(),
+        options -> options.enableCfFrontend = true);
+    ProcessResult processResult = ToolHelper.runJava(outputJar, main);
+    assertEquals(0, processResult.exitCode);
+    return processResult.stdout;
+  }
+
   private void expectDxFailure(JasminBuilder builder) throws Exception {
     // This expects this dx failure:
     // Uncaught translation error: com.android.dex.util.ExceptionWithContext: returning from
@@ -387,6 +409,8 @@
     assertEquals(expected, javaResult);
     String artResult = runOnArtD8(builder, clazz.name);
     assertEquals(expected, artResult);
+    String cfFrontendResult = runOnJavaR8CfFrontend(builder, clazz.name);
+    assertEquals(expected, cfFrontendResult);
     // This fails with dx.
     expectDxFailure(builder);
   }
@@ -1344,6 +1368,8 @@
     String artResult = runOnArtD8(builder, clazz.name);
     // The ASM jsr inliner does not get the control-flow dependent ret right in his case.
     assertNotEquals(expected, artResult);
+    String cfFrontendResult = runOnJavaR8CfFrontend(builder, clazz.name);
+    assertNotEquals(expected, cfFrontendResult);
     // This fails with dx.
     expectDxFailure(builder);
   }
@@ -1392,6 +1418,8 @@
     String artResult = runOnArtD8(builder, clazz.name);
     // The ASM jsr inliner does not get the control-flow dependent ret right in his case.
     assertNotEquals(expected, artResult);
+    String cfFrontendResult = runOnJavaR8CfFrontend(builder, clazz.name);
+    assertNotEquals(expected, cfFrontendResult);
     // This fails with dx.
     expectDxFailure(builder);
   }
@@ -1441,6 +1469,8 @@
     String artResult = runOnArtD8(builder, clazz.name);
     // The ASM jsr inliner does not get the control-flow dependent ret right in his case.
     assertNotEquals(expected, artResult);
+    String cfFrontendResult = runOnJavaR8CfFrontend(builder, clazz.name);
+    assertNotEquals(expected, cfFrontendResult);
     // This fails with dx.
     expectDxFailure(builder);
   }
diff --git a/src/test/java/com/android/tools/r8/jasmin/Regress80124071.java b/src/test/java/com/android/tools/r8/jasmin/Regress80124071.java
new file mode 100644
index 0000000..0eda515
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/jasmin/Regress80124071.java
@@ -0,0 +1,71 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.jasmin;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.jasmin.JasminBuilder.ClassFileVersion;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class Regress80124071 extends JasminTestBase {
+
+  private JasminBuilder buildClass() {
+    JasminBuilder builder = new JasminBuilder(ClassFileVersion.JDK_1_4);
+    JasminBuilder.ClassBuilder clazz = builder.addClass("Test");
+
+    clazz.addPrivateVirtualMethod("privateMethod", ImmutableList.of(), "V",
+        ".limit stack 2",
+        ".limit locals 1",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "ldc \"privateMethod\"",
+        "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+        "return");
+
+    clazz.addDefaultConstructor();
+
+    clazz.addMainMethod(
+        ".limit stack 3",
+        ".limit locals 1",
+        "new Test",
+        "dup",
+        "invokespecial Test/<init>()V",
+        // Should have been invokespecial but JVM is OK with it so we need to transform
+        // to invoke-direct to be able to run on Art.
+        "invokevirtual Test/privateMethod()V",
+        "new TestSub",
+        "dup",
+        "dup",
+        "invokespecial TestSub/<init>()V",
+        // Should have been invokespecial but JVM is OK with it and invokes the private method
+        // on the Test class. Therefore, there is no virtual dispatch and we need to transform
+        // to invoke-direct to be able to run on Art.
+        "invokevirtual Test/privateMethod()V",
+        "invokevirtual TestSub/privateMethod()V",
+        "return");
+
+    JasminBuilder.ClassBuilder subclazz = builder.addClass("TestSub", "Test");
+
+    subclazz.addVirtualMethod("privateMethod", ImmutableList.of(), "V",
+        ".limit stack 2",
+        ".limit locals 1",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "ldc \"privateMethod2\"",
+        "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+        "return");
+
+    return builder;
+  }
+
+  @Test
+  public void test() throws Exception {
+    JasminBuilder builder = buildClass();
+    String jvm = runOnArtDx(builder, "Test");
+    String dx = runOnJava(builder, "Test");
+    assertEquals(jvm, dx);
+    String d8 = runOnArtD8(builder, "Test");
+    assertEquals(jvm, d8);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 93f4249..bb83093 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.utils.DexInspector.FieldSubject;
 import com.android.tools.r8.utils.DexInspector.MethodSubject;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -34,6 +35,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import org.junit.Assume;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -183,13 +185,23 @@
         + "}";
   }
 
-  protected void runTest(String folder, String mainClass, AndroidAppInspector inspector)
-      throws Exception {
-    runTest(folder, mainClass, null, inspector);
+  protected void runTest(String folder, String mainClass,
+      AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, null, null, inspector);
+  }
+
+  protected void runTest(String folder, String mainClass,
+      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, null, optionsConsumer, inspector);
+  }
+
+  protected void runTest(String folder, String mainClass,
+      String extraProguardRules, AndroidAppInspector inspector) throws Exception {
+    runTest(folder, mainClass, extraProguardRules, null, inspector);
   }
 
   protected void runTest(String folder, String mainClass, String extraProguardRules,
-      AndroidAppInspector inspector) throws Exception {
+      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
     Assume.assumeTrue(ToolHelper.artSupported());
 
     String proguardRules = buildProguardRules(mainClass);
@@ -206,7 +218,7 @@
     // Build with R8
     AndroidApp.Builder builder = AndroidApp.builder();
     builder.addProgramFiles(classpath);
-    AndroidApp app = compileWithR8(builder.build(), proguardRules);
+    AndroidApp app = compileWithR8(builder.build(), proguardRules, optionsConsumer);
 
     // Materialize file for execution.
     Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
index 315e980..ee9065f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
@@ -252,7 +252,7 @@
   @Test
   public void testTrivialKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_trivial.MainKt";
-    runTest("lambdas_kstyle_trivial", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_trivial", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_trivial";
 
@@ -293,7 +293,7 @@
   @Test
   public void testCapturesKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_captures.MainKt";
-    runTest("lambdas_kstyle_captures", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_captures", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_captures";
       String grpPkg = allowAccessModification ? "" : pkg;
@@ -318,7 +318,7 @@
   @Test
   public void testGenericsNoSignatureKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_generics.MainKt";
-    runTest("lambdas_kstyle_generics", mainClassName, null, (app) -> {
+    runTest("lambdas_kstyle_generics", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_kstyle_generics";
       String grpPkg = allowAccessModification ? "" : pkg;
@@ -387,7 +387,7 @@
   @Test
   public void testTrivialJs() throws Exception {
     final String mainClassName = "lambdas_jstyle_trivial.MainKt";
-    runTest("lambdas_jstyle_trivial", mainClassName, null, (app) -> {
+    runTest("lambdas_jstyle_trivial", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_jstyle_trivial";
       String grp = allowAccessModification ? "" : pkg;
@@ -435,7 +435,7 @@
   @Test
   public void testSingleton() throws Exception {
     final String mainClassName = "lambdas_singleton.MainKt";
-    runTest("lambdas_singleton", mainClassName, null, (app) -> {
+    runTest("lambdas_singleton", mainClassName, (app) -> {
       Verifier verifier = new Verifier(app);
       String pkg = "lambdas_singleton";
       String grp = allowAccessModification ? "" : pkg;
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index cb3820e..f884376 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
 import com.android.tools.r8.utils.DexInspector.FieldSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import java.util.function.Consumer;
 import org.junit.Test;
 
 public class R8KotlinPropertiesTest extends AbstractR8KotlinTestBase {
@@ -57,11 +59,35 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
+  private static final TestKotlinClass OBJECT_PROPERTY_CLASS =
+      new TestKotlinClass("properties.ObjectProperties")
+          .addProperty("privateProp", JAVA_LANG_STRING, Visibility.PRIVATE)
+          .addProperty("protectedProp", JAVA_LANG_STRING, Visibility.PROTECTED)
+          .addProperty("internalProp", JAVA_LANG_STRING, Visibility.INTERNAL)
+          .addProperty("publicProp", JAVA_LANG_STRING, Visibility.PUBLIC)
+          .addProperty("primitiveProp", "int", Visibility.PUBLIC)
+          .addProperty("privateLateInitProp", JAVA_LANG_STRING, Visibility.PRIVATE)
+          .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
+          .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
+
+  private static final TestFileLevelKotlinClass FILE_PROPERTY_CLASS =
+      new TestFileLevelKotlinClass("properties.FilePropertiesKt")
+          .addProperty("privateProp", JAVA_LANG_STRING, Visibility.PRIVATE)
+          .addProperty("protectedProp", JAVA_LANG_STRING, Visibility.PROTECTED)
+          .addProperty("internalProp", JAVA_LANG_STRING, Visibility.INTERNAL)
+          .addProperty("publicProp", JAVA_LANG_STRING, Visibility.PUBLIC)
+          .addProperty("primitiveProp", "int", Visibility.PUBLIC)
+          .addProperty("privateLateInitProp", JAVA_LANG_STRING, Visibility.PRIVATE)
+          .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
+          .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
+
+  private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+
   @Test
   public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -78,7 +104,7 @@
   public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -100,7 +126,7 @@
   public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -123,7 +149,7 @@
   public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -146,7 +172,7 @@
   public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -169,7 +195,7 @@
   public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -194,7 +220,7 @@
   public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -211,7 +237,7 @@
   public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -234,7 +260,7 @@
   public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -255,7 +281,7 @@
   public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -274,7 +300,7 @@
   public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -293,7 +319,7 @@
   public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -310,7 +336,7 @@
   public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject classSubject = checkClassExists(dexInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -336,7 +362,7 @@
   public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -367,7 +393,7 @@
   public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -401,7 +427,7 @@
   public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -432,7 +458,7 @@
   public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector,
           "properties.CompanionProperties");
@@ -464,7 +490,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
@@ -495,7 +521,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
@@ -519,7 +545,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
       DexInspector dexInspector = new DexInspector(app);
       ClassSubject outerClass = checkClassExists(dexInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassExists(dexInspector, testedClass.getClassName());
@@ -537,4 +563,363 @@
       assertTrue(fieldSubject.getField().accessFlags.isPublic());
     });
   }
+
+  @Test
+  public void testObjectClass_primitivePropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "primitiveProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, "int", propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Getter and setter cannot be inlined when we don't know if null check semantic is
+      // preserved.
+      checkMethodIsPresent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testObjectClass_privatePropertyIsAlwaysInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "privateProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // A private property has no getter/setter.
+      checkMethodIsAbsent(objectClass, getter);
+      checkMethodIsAbsent(objectClass, setter);
+
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testObjectClass_internalPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "internalProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Getter and setter cannot be inlined when we don't know if null check semantic is
+      // preserved.
+      checkMethodIsPresent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testObjectClass_publicPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "publicProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Getter and setter cannot be inlined when we don't know if null check semantic is
+      // preserved.
+      checkMethodIsPresent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testObjectClass_privateLateInitPropertyIsAlwaysInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "privateLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // A private property has no getter/setter.
+      checkMethodIsAbsent(objectClass, getter);
+      checkMethodIsAbsent(objectClass, setter);
+
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testObjectClass_internalLateInitPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "internalLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Getter and setter cannot be inlined when we don't know if null check semantic is
+      // preserved.
+      checkMethodIsPresent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+    });
+  }
+
+  @Test
+  public void testObjectClass_publicLateInitPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "publicLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Getter and setter cannot be inlined when we don't know if null check semantic is
+      // preserved.
+      checkMethodIsPresent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+    });
+  }
+
+  @Test
+  public void testFileLevel_primitivePropertyIsInlinedIfAccessIsRelaxed() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "primitiveProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, "int", propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+        checkMethodIsAbsent(objectClass, getter);
+        checkMethodIsAbsent(objectClass, setter);
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+        checkMethodIsPresent(objectClass, getter);
+        checkMethodIsPresent(objectClass, setter);
+      }
+    });
+  }
+
+  @Test
+  public void testFileLevel_privatePropertyIsAlwaysInlined() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_usePrivateProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "privateProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // A private property has no getter/setter.
+      checkMethodIsAbsent(objectClass, getter);
+      checkMethodIsAbsent(objectClass, setter);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testFileLevel_internalPropertyGetterIsInlinedIfAccessIsRelaxed() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_useInternalProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "internalProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      // We expect getter to be inlined when access (of the backing field) is relaxed to public.
+      // Note: the setter is considered as a regular method (because of KotlinC adding extra null
+      // checks), thus we cannot say if the setter would be inlined or not by R8.
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+        checkMethodIsAbsent(objectClass, getter);
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+        checkMethodIsPresent(objectClass, getter);
+      }
+    });
+  }
+
+  @Test
+  public void testFileLevel_publicPropertyGetterIsInlinedIfAccessIsRelaxed() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_usePublicProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "publicProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      // We expect getter to be inlined when access (of the backing field) is relaxed to public.
+      // On the other hand, the setter is considered as a regular method (because of null checks),
+      // thus we cannot say if it can be inlined or not.
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+        checkMethodIsAbsent(objectClass, getter);
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+        checkMethodIsPresent(objectClass, getter);
+      }
+    });
+  }
+
+  @Test
+  public void testFileLevel_privateLateInitPropertyIsAlwaysInlined() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject fileClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "privateLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(fileClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // A private property has no getter/setter.
+      checkMethodIsAbsent(fileClass, getter);
+      checkMethodIsAbsent(fileClass, setter);
+      if (allowAccessModification) {
+        assertTrue(fieldSubject.getField().accessFlags.isPublic());
+      } else {
+        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+      }
+    });
+  }
+
+  @Test
+  public void testFileLevel_internalLateInitPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "internalLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      // Field is public and getter is small so we expect to always inline it.
+      checkMethodIsAbsent(objectClass, getter);
+
+      // Setter has null check of new value, thus may not be inlined.
+      checkMethodIsPresent(objectClass, setter);
+    });
+  }
+
+  @Test
+  public void testFileLevel_publicLateInitPropertyCannotBeInlined() throws Exception {
+    final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
+    String mainClass = addMainToClasspath(
+        "properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
+    runTest(PACKAGE_NAME, mainClass, (app) -> {
+      DexInspector dexInspector = new DexInspector(app);
+      ClassSubject objectClass = checkClassExists(dexInspector, testedClass.getClassName());
+      String propertyName = "publicLateInitProp";
+      FieldSubject fieldSubject = checkFieldIsPresent(objectClass, JAVA_LANG_STRING, propertyName);
+      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+
+      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+      MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+
+      checkMethodIsAbsent(objectClass, getter);
+      checkMethodIsPresent(objectClass, setter);
+      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+    });
+  }
+
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/TestFileLevelKotlinClass.java b/src/test/java/com/android/tools/r8/kotlin/TestFileLevelKotlinClass.java
new file mode 100644
index 0000000..db35897
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/TestFileLevelKotlinClass.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.kotlin;
+
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+
+public class TestFileLevelKotlinClass extends TestKotlinClass {
+
+  public TestFileLevelKotlinClass(String className) {
+    super(className);
+  }
+
+  @Override
+  public TestFileLevelKotlinClass addProperty(String name, String type, Visibility visibility) {
+    return (TestFileLevelKotlinClass) super.addProperty(name, type, visibility);
+  }
+
+  @Override
+  public MethodSignature getGetterForProperty(String propertyName) {
+    KotlinProperty property = getProperty(propertyName);
+    // File-level properties accessor do not apply mangling.
+    return getGetterForProperty(property, false);
+  }
+
+  @Override
+  public MethodSignature getSetterForProperty(String propertyName) {
+    KotlinProperty property = getProperty(propertyName);
+    // File-level properties accessor do not apply mangling.
+    return getSetterForProperty(property, false);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java b/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
index 0ec0f64..685df6a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
+++ b/src/test/java/com/android/tools/r8/kotlin/TestKotlinClass.java
@@ -104,22 +104,12 @@
 
   public MemberNaming.MethodSignature getGetterForProperty(String propertyName) {
     KotlinProperty property = getProperty(propertyName);
-    String type = property.type;
-    String getterName = computeGetterName(propertyName);
-    if (property.getVisibility() == Visibility.INTERNAL) {
-      getterName = appendInternalSuffix(getterName);
-    }
-    return new MemberNaming.MethodSignature(getterName, type, Collections.emptyList());
+    return getGetterForProperty(property, property.getVisibility() == Visibility.INTERNAL);
   }
 
   public MemberNaming.MethodSignature getSetterForProperty(String propertyName) {
     KotlinProperty property = getProperty(propertyName);
-    String setterName = computeSetterName(propertyName);
-    if (property.getVisibility() == Visibility.INTERNAL) {
-      setterName = appendInternalSuffix(setterName);
-    }
-    return new MemberNaming.MethodSignature(setterName, "void",
-        Collections.singleton(property.getType()));
+    return getSetterForProperty(property, property.getVisibility() == Visibility.INTERNAL);
   }
 
   public MemberNaming.MethodSignature getGetterAccessorForProperty(String propertyName,
@@ -152,6 +142,26 @@
     return new MemberNaming.MethodSignature(setterName, "void", argumentTypes);
   }
 
+  protected final MemberNaming.MethodSignature getGetterForProperty(KotlinProperty property,
+      boolean applyMangling) {
+    String type = property.type;
+    String getterName = computeGetterName(property.name);
+    if (applyMangling) {
+      getterName = appendInternalSuffix(getterName);
+    }
+    return new MemberNaming.MethodSignature(getterName, type, Collections.emptyList());
+  }
+
+  protected final MemberNaming.MethodSignature getSetterForProperty(KotlinProperty property,
+      boolean applyMangling) {
+    String setterName = computeSetterName(property.name);
+    if (applyMangling) {
+      setterName = appendInternalSuffix(setterName);
+    }
+    return new MemberNaming.MethodSignature(setterName, "void",
+        Collections.singleton(property.getType()));
+  }
+
   private static String computeGetterName(String propertyName) {
     if (propertyName.length() > 2 && propertyName.startsWith("is")
         && (propertyName.charAt(2) == '_' || Character.isUpperCase(propertyName.charAt(2)))) {
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 d6b26f0..590baa6 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -25,11 +25,11 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.DexOverflowException;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -40,6 +40,7 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Position;
@@ -617,9 +618,9 @@
                     DexString.EMPTY_ARRAY),
                 access,
                 DexAnnotationSet.empty(),
-                DexAnnotationSetRefList.empty(),
+                ParameterAnnotationsList.empty(),
                 code);
-        IRCode ir = code.buildIR(method, options);
+        IRCode ir = code.buildIR(method, null, options, Origin.unknown());
         RegisterAllocator allocator = new LinearScanRegisterAllocator(ir, options);
         method.setCode(ir, allocator, options);
         directMethods[i] = method;
diff --git a/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java b/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
index 490162a..3429c2a 100644
--- a/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
+++ b/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -30,7 +29,6 @@
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class));
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(Utils.class));
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    builder.setMinApiLevel(AndroidApiLevel.O.getLevel());
     builder.setMode(CompilationMode.RELEASE);
     builder.addProguardConfiguration(
         ImmutableList.of(
diff --git a/src/test/java/com/android/tools/r8/movestringconstants/TestClass.java b/src/test/java/com/android/tools/r8/movestringconstants/TestClass.java
index bf13fc3..bf74250 100644
--- a/src/test/java/com/android/tools/r8/movestringconstants/TestClass.java
+++ b/src/test/java/com/android/tools/r8/movestringconstants/TestClass.java
@@ -5,6 +5,8 @@
 package com.android.tools.r8.movestringconstants;
 
 public class TestClass {
+  public static void main(String[] args) {}
+
   static void foo(String arg1, String arg2, String arg3, String arg4) {
     Utils.check(arg1, "StringConstants::foo#1");
     Utils.check("", "StringConstants::foo#2");
diff --git a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
index 3b2cf8c..e1fa3d0 100644
--- a/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/InterfaceRenamingTestRunner.java
@@ -100,11 +100,6 @@
     for (Class<?> c : CLASSES) {
       builder.addClassProgramData(ToolHelper.getClassAsBytes(c), Origin.unknown());
     }
-    if (consumer instanceof ClassFileConsumer) {
-      // TODO(b/75997473): Enable inlining when supported by CF backend
-      ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
-    } else {
-      ToolHelper.runR8(builder.build());
-    }
+    ToolHelper.runR8(builder.build());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/LambdaRenamingTestRunner.java b/src/test/java/com/android/tools/r8/naming/LambdaRenamingTestRunner.java
index 9e062b3..c7c74e9 100644
--- a/src/test/java/com/android/tools/r8/naming/LambdaRenamingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/naming/LambdaRenamingTestRunner.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import java.io.IOException;
@@ -119,13 +118,10 @@
             .addProgramFiles(inputJar)
             .setProgramConsumer(consumer)
             .addProguardConfigurationFiles(writeProguardRules(aggressive));
-    if (consumer instanceof ClassFileConsumer) {
-      // TODO(b/75997473): Enable inlining when supported by CF backend
-      ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
-    } else {
+    if (!(consumer instanceof ClassFileConsumer)) {
       builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
-      ToolHelper.runR8(builder.build());
     }
+    ToolHelper.runR8(builder.build());
   }
 
   private void buildAndRunProguard(String outName, boolean aggressive) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
index 4a7ba49..1f9c20f 100644
--- a/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
+++ b/src/test/java/com/android/tools/r8/neverreturnsnormally/NeverReturnsNormallyTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DexInspector;
 import com.android.tools.r8.utils.DexInspector.ClassSubject;
@@ -31,7 +30,6 @@
     R8Command.Builder builder = R8Command.builder();
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class));
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    builder.setMinApiLevel(AndroidApiLevel.O.getLevel());
     builder.setMode(mode);
     builder.addProguardConfiguration(
         ImmutableList.of(
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
index 9026740..a185f4c 100644
--- a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
@@ -33,7 +33,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     DexInspector inspector = new DexInspector(app);
     List<FoundClassSubject> classes = inspector.allClasses();
 
@@ -66,7 +66,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     DexInspector inspector = new DexInspector(app);
     List<FoundClassSubject> classes = inspector.allClasses();
 
diff --git a/src/test/java/com/android/tools/r8/regress/b72485384/Main.java b/src/test/java/com/android/tools/r8/regress/b72485384/Main.java
index c569c5e..f492fcc 100644
--- a/src/test/java/com/android/tools/r8/regress/b72485384/Main.java
+++ b/src/test/java/com/android/tools/r8/regress/b72485384/Main.java
@@ -3,14 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.regress.b72485384;
 
-import com.google.common.base.Functions;
+import java.util.function.Function;
 
 public class Main {
 
   public static void main(String[] args) {
     GenericOuter<String> outer = new GenericOuter<>();
-    GenericOuter<String>.GenericInner<String> inner = outer
-        .makeInner(Functions.identity(), "Hello World!");
+    GenericOuter<String>.GenericInner<String> inner =
+        outer.makeInner(Function.identity(), "Hello World!");
     System.out.println(inner.innerGetter(inner));
     System.out.println(outer.outerGetter(inner));
   }
diff --git a/src/test/java/com/android/tools/r8/regress/b72485384/Regress72485384Test.java b/src/test/java/com/android/tools/r8/regress/b72485384/Regress72485384Test.java
index 3205434..ba65d92 100644
--- a/src/test/java/com/android/tools/r8/regress/b72485384/Regress72485384Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b72485384/Regress72485384Test.java
@@ -3,47 +3,70 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.regress.b72485384;
 
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.regress.b72485384.GenericOuter.GenericInner;
 import com.android.tools.r8.utils.AndroidApp;
 import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class Regress72485384Test extends TestBase {
+  @Rule public ExpectedException thrown = ExpectedException.none();
 
   @Parameters(name = "{0}")
-  public static List<String> getParameters() {
+  public static Collection<Object[]> getParameters() {
     String baseConfig =
         keepMainProguardConfiguration(Main.class)
             + "-keepattributes Signature,InnerClasses,EnclosingMethod ";
-    return ImmutableList.of(
-        baseConfig,
-        baseConfig + "-dontshrink",
-        baseConfig + "-dontshrink -dontobfuscate",
-        baseConfig + "-dontobfuscate",
-        "",
-        "-dontshrink",
-        "-keep class DoesNotExist -dontshrink"
-    );
+    return Arrays.asList(
+        new Object[][] {
+          {baseConfig, null},
+          {baseConfig + "-dontshrink", null},
+          {baseConfig + "-dontshrink -dontobfuscate", null},
+          {baseConfig + "-dontobfuscate", null},
+          {"", null},
+          {"-dontshrink", null},
+          {"-keep class DoesNotExist -dontshrink", "ClassNotFoundException"}
+        });
   }
 
   private final static List<Class> CLASSES = ImmutableList
       .of(GenericOuter.class, GenericInner.class, Main.class);
 
   private final String proguardConfig;
+  private final String expectedErrorMessage;
 
-  public Regress72485384Test(String proguardConfig) {
+  public Regress72485384Test(String proguardConfig, String expectedErrorMessage) {
     this.proguardConfig = proguardConfig;
+    this.expectedErrorMessage = expectedErrorMessage;
   }
 
   @Test
   public void testSignatureRewrite() throws Exception {
-    AndroidApp result = compileWithR8(CLASSES, proguardConfig);
-    runOnArt(result, Main.class.getCanonicalName());
+    AndroidApp app = compileWithR8(CLASSES, proguardConfig);
+
+    if (expectedErrorMessage == null) {
+      if (ToolHelper.getDexVm().getVersion().isOlderThanOrEqual(ToolHelper.DexVm.Version.V6_0_1)) {
+        // Resolution of java.util.function.Function fails.
+        thrown.expect(AssertionError.class);
+      }
+
+      runOnArt(app, Main.class.getCanonicalName());
+    } else {
+      ToolHelper.ProcessResult result = runOnArtRaw(app, Main.class.getCanonicalName());
+      assertThat(result.stderr, containsString(expectedErrorMessage));
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java b/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
index 3e75ac6..d1fc83e 100644
--- a/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
+++ b/src/test/java/com/android/tools/r8/resolution/PublicFieldInnerClassTestRunner.java
@@ -69,11 +69,6 @@
     for (Class<?> c : CLASSES) {
       builder.addClassProgramData(ToolHelper.getClassAsBytes(c), Origin.unknown());
     }
-    if (consumer instanceof ClassFileConsumer) {
-      // TODO(b/75997473): Enable inlining when supported by CF.
-      ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
-    } else {
-      ToolHelper.runR8(builder.build());
-    }
+    ToolHelper.runR8(builder.build());
   }
 }
diff --git a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
index 167262f..dbd4e23 100644
--- a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
+++ b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
@@ -6,7 +6,7 @@
 
 import com.android.tools.r8.CompilationException;
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -35,9 +35,10 @@
     ProcessResult referenceResult = ToolHelper.runJava(inputJar, mainClassName);
 
     Path r8Out = temp.getRoot().toPath().resolve("r8out.jar");
-    R8Command.Builder builder = R8Command.builder()
-        .addProgramFiles(inputJar)
-        .setOutput(r8Out, OutputMode.DexIndexed);
+    R8Command.Builder builder =
+        R8Command.builder()
+            .addProgramFiles(inputJar)
+            .setProgramConsumer(new DexIndexedConsumer.ArchiveConsumer(r8Out, true));
     ToolHelper.runR8(builder.build());
 
     ProcessResult r8Result = ToolHelper.runArtRaw(r8Out.toString(), mainClassName);
diff --git a/src/test/java/com/android/tools/r8/shaking/InstantiatedLambdasTestRunner.java b/src/test/java/com/android/tools/r8/shaking/InstantiatedLambdasTestRunner.java
index 27f5321..474af4d 100644
--- a/src/test/java/com/android/tools/r8/shaking/InstantiatedLambdasTestRunner.java
+++ b/src/test/java/com/android/tools/r8/shaking/InstantiatedLambdasTestRunner.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import java.io.IOException;
@@ -112,13 +111,10 @@
             .addProgramFiles(inputJar)
             .setProgramConsumer(consumer)
             .addProguardConfigurationFiles(writeProguardRules(aggressive));
-    if (consumer instanceof ClassFileConsumer) {
-      // TODO(b/75997473): Enable inlining when supported by CF backend
-      ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
-    } else {
+    if (!(consumer instanceof ClassFileConsumer)) {
       builder.setMinApiLevel(ToolHelper.getMinApiLevelForDexVm().getLevel());
-      ToolHelper.runR8(builder.build());
     }
+    ToolHelper.runR8(builder.build());
   }
 
   private void buildAndRunProguard(String outName, boolean aggressive) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
new file mode 100644
index 0000000..6ecdb09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharSource;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import org.junit.Test;
+
+class A {
+  private static String buildClassName(String className) {
+    return A.class.getPackage().getName() + "." + className;
+  }
+
+  public static void main(String[] args) {
+    try {
+      Class bClass = Class.forName(buildClassName("B"));
+      System.out.println("YES");
+    } catch (ClassNotFoundException e) {
+      System.out.println("NO");
+    }
+  }
+}
+
+class B {
+
+}
+
+public class LibraryProvidedProguardRulesTest extends TestBase {
+  private void addTextJarEntry(JarOutputStream out, String name, String content) throws Exception {
+    out.putNextEntry(new ZipEntry(name));
+    ByteStreams.copy(
+        CharSource.wrap(content).asByteSource(StandardCharsets.UTF_8).openBufferedStream(), out);
+    out.closeEntry();
+  }
+
+  private AndroidApp runTest(List<String> rules, DiagnosticsHandler handler) throws Exception {
+    Path jar = temp.newFile("test.jar").toPath();
+    try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
+      addTestClassesToJar(out, A.class, B.class);
+      for (int i =  0; i < rules.size(); i++) {
+        String name = "META-INF/proguard/jar" + (i == 0 ? "" : i) + ".rules";
+        addTextJarEntry(out, name, rules.get(i));
+      }
+    }
+
+    try {
+      R8Command command = (handler != null ? R8Command.builder(handler) : R8Command.builder())
+          .addProgramFiles(jar)
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .build();
+      return ToolHelper.runR8(command);
+    } catch (CompilationFailedException e) {
+      assertNotNull(handler);
+      return null;
+    }
+  }
+
+  private AndroidApp runTest(String rules, DiagnosticsHandler handler) throws Exception {
+    return runTest(ImmutableList.of(rules), handler);
+  }
+
+
+  @Test
+  public void keepOnlyA() throws Exception {
+    AndroidApp app = runTest("-keep class " + A.class.getTypeName() +" {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), not(isPresent()));
+  }
+
+  @Test
+  public void keepOnlyB() throws Exception {
+    AndroidApp app = runTest("-keep class **B {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), not(isPresent()));
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  @Test
+  public void keepBoth() throws Exception {
+    AndroidApp app = runTest("-keep class ** {}", null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  @Test
+  public void multipleFiles() throws Exception {
+    AndroidApp app = runTest(ImmutableList.of("-keep class **A {}", "-keep class **B {}"), null);
+    DexInspector inspector = new DexInspector(app);
+    assertThat(inspector.clazz(A.class), isPresent());
+    assertThat(inspector.clazz(B.class), isPresent());
+  }
+
+  private void checkOrigin(Origin origin) {
+    assertTrue(origin instanceof ArchiveEntryOrigin);
+    assertEquals(origin.part(), "META-INF/proguard/jar.rules");
+    assertTrue(origin.parent() instanceof PathOrigin);
+  }
+
+  @Test
+  public void syntaxError() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    AndroidApp app = runTest("error", checker);
+    assertNull(app);
+    DiagnosticsChecker.checkDiagnostic(
+        checker.errors.get(0), this::checkOrigin, 1, 1, "Expected char '-'");
+  }
+
+  @Test
+  public void includeError() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    AndroidApp app = runTest("-include other.rules", checker);
+    assertNull(app);
+    DiagnosticsChecker.checkDiagnostic(checker.errors.get(0), this::checkOrigin, 1, 10,
+        "Options with file names are not supported");
+  }
+
+  class TestProvider implements ProgramResourceProvider, DataResourceProvider {
+
+    @Override
+    public Collection<ProgramResource> getProgramResources() throws ResourceException {
+      byte[] bytes;
+      try {
+        bytes = ByteStreams.toByteArray(A.class.getResourceAsStream("A.class"));
+      } catch (IOException e) {
+        throw new ResourceException(Origin.unknown(), "Unexpected");
+      }
+      return ImmutableList.of(
+          ProgramResource.fromBytes(Origin.unknown(), Kind.CF, bytes,
+              Collections.singleton(DescriptorUtils.javaTypeToDescriptor(A.class.getTypeName()))));
+    }
+
+    @Override
+    public DataResourceProvider getDataResourceProvider() {
+      return this;
+    }
+
+    @Override
+    public void accept(Visitor visitor) throws ResourceException {
+      throw new ResourceException(Origin.unknown(), "Cannot provide data resources after all");
+    }
+  }
+
+  @Test
+  public void throwingDataResourceProvider() throws Exception {
+    DiagnosticsChecker checker = new DiagnosticsChecker();
+    try {
+      R8Command command = R8Command.builder(checker)
+          .addProgramResourceProvider(new TestProvider())
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .build();
+      fail("Should not succeed");
+    } catch (CompilationFailedException e) {
+      DiagnosticsChecker.checkDiagnostic(
+          checker.errors.get(0),
+          origin -> assertSame(origin, Origin.unknown()),
+          "Cannot provide data resources after all");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index b7277cb..dd7465f 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1681,6 +1681,17 @@
   }
 
   @Test
+  public void parse_mergeinterfaceaggressively() throws Exception {
+    Path proguardConfig = writeTextToTempFile(
+        "-mergeinterfacesaggressively"
+    );
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(new DexItemFactory(), reporter);
+    parser.parse(proguardConfig);
+    verifyParserEndsCleanly();
+  }
+
+  @Test
   public void parse_addconfigurationdebugging() throws Exception {
     Path proguardConfig = writeTextToTempFile(
         "-addconfigurationdebugging"
@@ -1718,6 +1729,7 @@
     assertEquals(0, handler.errors.size());
   }
 
+  // TODO(sgjesse): Change to use DiagnosticsChecker.
   private Diagnostic checkDiagnostic(List<Diagnostic> diagnostics, Path path, int lineStart,
       int columnStart, String... messageParts) {
     assertEquals(1, diagnostics.size());
diff --git a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
index 3de225d..5845dd3 100644
--- a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
@@ -39,7 +39,7 @@
         "-dontobfuscate"),
         Origin.unknown());
     builder.addProguardConfiguration(additionalKeepRules, Origin.unknown());
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     inspection.accept(new DexInspector(app));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingInliningTest.java b/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingInliningTest.java
index 8c67135..b77b29e 100644
--- a/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingInliningTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/examples/TreeShakingInliningTest.java
@@ -21,7 +21,7 @@
   public static Collection<Object[]> data() {
     List<Object[]> parameters = new ArrayList<>();
     for (MinifyMode minify : MinifyMode.values()) {
-      // TODO(b/75997473): Add Frontend.JAR, Backend.CF when inlining is supported in CF backend.
+      parameters.add(new Object[] {Frontend.JAR, Backend.CF, minify});
       parameters.add(new Object[] {Frontend.JAR, Backend.DEX, minify});
       parameters.add(new Object[] {Frontend.DEX, Backend.DEX, minify});
     }
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index 1151648..91fc9d4 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -599,7 +599,7 @@
     AndroidApp app;
     builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
     try {
-      app = ToolHelper.runR8(builder.build());
+      app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     } catch (CompilationError e) {
       assertTrue(!forceProguardCompatibility && (!innerClasses || !enclosingMethod));
       return;
@@ -656,7 +656,7 @@
     builder.setMinApiLevel(AndroidApiLevel.O.getLevel());
     Path proguardCompatibilityRules = temp.newFile().toPath();
     builder.setProguardCompatibilityRulesOutput(proguardCompatibilityRules);
-    AndroidApp app = ToolHelper.runR8(builder.build());
+    AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
     inspection.accept(new DexInspector(app));
     // Check the Proguard compatibility configuration generated.
     ProguardConfigurationParser parser =
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 d2391ff..6b90d88 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -67,7 +68,7 @@
 
     DexEncodedMethod method = getMethod(originalApplication, methodSig);
     // Get the IR pre-optimization.
-    IRCode code = method.buildIR(new InternalOptions());
+    IRCode code = method.buildIR(null, new InternalOptions(), Origin.unknown());
 
     // Find the exit block and assert that the value is a phi merging the exceptional edge
     // with the normal edge.
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 29676da..ace5c9b 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -136,6 +136,19 @@
             .read(app.getProguardMapOutputData()));
   }
 
+  public DexInspector(AndroidApp app, Consumer<InternalOptions> optionsConsumer)
+      throws IOException, ExecutionException {
+    this(
+        new ApplicationReader(app, runOptionsConsumer(optionsConsumer), new Timing("DexInspector"))
+            .read(app.getProguardMapOutputData()));
+  }
+
+  private static InternalOptions runOptionsConsumer(Consumer<InternalOptions> optionsConsumer) {
+    InternalOptions internalOptions = new InternalOptions();
+    optionsConsumer.accept(internalOptions);
+    return internalOptions;
+  }
+
   public DexInspector(AndroidApp app, Path proguardMap) throws IOException, ExecutionException {
     this(
         new ApplicationReader(app, new InternalOptions(), new Timing("DexInspector"))
diff --git a/src/test/kotlinR8TestResources/properties/FileProperties.kt b/src/test/kotlinR8TestResources/properties/FileProperties.kt
new file mode 100644
index 0000000..4671189
--- /dev/null
+++ b/src/test/kotlinR8TestResources/properties/FileProperties.kt
@@ -0,0 +1,82 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package properties
+
+private var privateProp: String = "privateProp"
+internal var internalProp: String = "internalProp"
+public var publicProp: String = "publicProp"
+
+private lateinit var privateLateInitProp: String
+internal lateinit var internalLateInitProp: String
+public lateinit var publicLateInitProp: String
+
+public var primitiveProp: Int = Int.MAX_VALUE
+
+// Serves as intermediate to make sure the access to a property is done from a separate class.
+private object Intermediate {
+    fun readPrivateProp() = privateProp
+    fun writePrivateProp(s: String) { privateProp = s }
+
+    fun readInternalProp() = internalProp
+    fun writeInternalProp(s: String) { internalProp = s }
+
+    fun readPublicProp() = publicProp
+    fun writePublicProp(s: String) { publicProp = s }
+
+    fun readPrimitiveProp() = primitiveProp
+    fun writePrimitiveProp(i: Int) { primitiveProp = i }
+
+    fun readLateInitPrivateProp() = privateLateInitProp
+    fun writeLateInitPrivateProp(s: String) { privateLateInitProp = s }
+
+    fun readLateInitInternalProp() = internalLateInitProp
+    fun writeLateInitInternalProp(s: String) { internalLateInitProp = s }
+
+    fun readLateInitPublicProp() = publicLateInitProp
+    fun writeLateInitPublicProp(s: String) { publicLateInitProp = s }
+}
+
+fun doNotUseProperties(): String {
+    return "doNotUseProperties"
+}
+
+fun fileProperties_noUseOfProperties() {
+    println(ObjectProperties.doNotUseProperties())
+}
+
+fun fileProperties_usePrivateProp() {
+    Intermediate.writePrivateProp("foo")
+    println(Intermediate.readPrivateProp())
+}
+
+fun fileProperties_useInternalProp() {
+    Intermediate.writeInternalProp("foo")
+    println(Intermediate.readInternalProp())
+}
+
+fun fileProperties_usePublicProp() {
+    Intermediate.writePublicProp("foo")
+    println(Intermediate.readPublicProp())
+}
+
+fun fileProperties_usePrimitiveProp() {
+    Intermediate.writePrimitiveProp(Int.MIN_VALUE)
+    println(Intermediate.readPrimitiveProp())
+}
+
+fun fileProperties_useLateInitPrivateProp() {
+    Intermediate.writeLateInitPrivateProp("foo")
+    println(Intermediate.readLateInitPrivateProp())
+}
+
+fun fileProperties_useLateInitInternalProp() {
+    Intermediate.writeLateInitInternalProp( "foo")
+    println(Intermediate.readLateInitInternalProp())
+}
+
+fun fileProperties_useLateInitPublicProp() {
+    Intermediate.writeLateInitPublicProp("foo")
+    println(Intermediate.readLateInitPublicProp())
+}
diff --git a/src/test/kotlinR8TestResources/properties/ObjectProperties.kt b/src/test/kotlinR8TestResources/properties/ObjectProperties.kt
new file mode 100644
index 0000000..d05e2a6
--- /dev/null
+++ b/src/test/kotlinR8TestResources/properties/ObjectProperties.kt
@@ -0,0 +1,76 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package properties
+
+object ObjectProperties {
+    private var privateProp: String = "privateProp"
+    internal var internalProp: String = "internalProp"
+    public var publicProp: String = "publicProp"
+
+    private lateinit var privateLateInitProp: String
+    internal lateinit var internalLateInitProp: String
+    public lateinit var publicLateInitProp: String
+
+    public var primitiveProp: Int = Int.MAX_VALUE
+
+    fun callSetterPrivateProp(v: String) {
+        privateProp = v
+    }
+
+    fun callGetterPrivateProp(): String {
+        return privateProp
+    }
+
+    fun callSetterLateInitPrivateProp(v: String) {
+        privateLateInitProp = v
+    }
+
+    fun callGetterLateInitPrivateProp(): String {
+        return privateLateInitProp
+    }
+
+    fun doNotUseProperties(): String {
+        return "doNotUseProperties"
+    }
+}
+
+fun objectProperties_noUseOfProperties() {
+    println(ObjectProperties.doNotUseProperties())
+}
+
+fun objectProperties_usePrivateProp() {
+    ObjectProperties.callSetterPrivateProp("foo")
+    println(ObjectProperties.callGetterPrivateProp())
+}
+
+fun objectProperties_useInternalProp() {
+    ObjectProperties.internalProp = "foo"
+    println(ObjectProperties.internalProp)
+}
+
+fun objectProperties_usePublicProp() {
+    ObjectProperties.publicProp = "foo"
+    println(ObjectProperties.publicProp)
+}
+
+fun objectProperties_usePrimitiveProp() {
+    ObjectProperties.primitiveProp = Int.MIN_VALUE
+    println(ObjectProperties.primitiveProp)
+}
+
+fun objectProperties_useLateInitPrivateProp() {
+    ObjectProperties.callSetterLateInitPrivateProp("foo")
+    println(ObjectProperties.callGetterLateInitPrivateProp())
+}
+
+fun objectProperties_useLateInitInternalProp() {
+    ObjectProperties.internalLateInitProp = "foo"
+    println(ObjectProperties.internalLateInitProp)
+}
+
+fun objectProperties_useLateInitPublicProp() {
+    ObjectProperties.publicLateInitProp = "foo"
+    println(ObjectProperties.publicLateInitProp)
+}
diff --git a/tools/archive.py b/tools/archive.py
index 815e416..07f138a 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -3,22 +3,25 @@
 # 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 gradle
 import create_maven_release
-import d8
+import gradle
 import os
-import r8
+import shutil
 import subprocess
 import sys
+import toolhelper
 import utils
-import shutil
 import zipfile
 
 ARCHIVE_BUCKET = 'r8-releases'
 
+def GetToolVersion(jar_path):
+  output = subprocess.check_output(['java', '-jar', jar_path, '--version'])
+  return output.splitlines()[0].strip()
+
 def GetVersion():
-  r8_version = r8.run(['--version'], build = False).splitlines()[0].strip()
-  d8_version = d8.run(['--version'], build = False).splitlines()[0].strip()
+  r8_version = GetToolVersion(utils.R8_JAR)
+  d8_version = GetToolVersion(utils.D8_JAR)
   # The version printed is "D8 vVERSION_NUMBER" and "R8 vVERSION_NUMBER"
   # Sanity check that versions match.
   if d8_version.split()[1] != r8_version.split()[1]:
@@ -49,21 +52,30 @@
                       'in a commit that have -dev in version')
   return True
 
-def GetStorageDestination(storage_prefix, version, file_name, is_master):
+def GetStorageDestination(storage_prefix,
+                          version_or_path,
+                          file_name,
+                          is_master):
   # We archive master commits under raw/master instead of directly under raw
-  version_dir = GetVersionDestination(storage_prefix, version, is_master)
+  version_dir = GetVersionDestination(storage_prefix,
+                                      version_or_path,
+                                      is_master)
   return '%s/%s' % (version_dir, file_name)
 
-def GetVersionDestination(storage_prefix, version, is_master):
+def GetVersionDestination(storage_prefix, version_or_path, is_master):
   archive_dir = 'raw/master' if is_master else 'raw'
-  return '%s%s/%s/%s' % (storage_prefix, ARCHIVE_BUCKET, archive_dir, version)
+  return '%s%s/%s/%s' % (storage_prefix, ARCHIVE_BUCKET,
+                         archive_dir, version_or_path)
 
-def GetUploadDestination(version, file_name, is_master):
-  return GetStorageDestination('gs://', version, file_name, is_master)
+def GetUploadDestination(version_or_path, file_name, is_master):
+  return GetStorageDestination('gs://', version_or_path, file_name, is_master)
 
-def GetUrl(version, file_name, is_master):
+def GetUrl(version_or_path, file_name, is_master):
   return GetStorageDestination('http://storage.googleapis.com/',
-                               version, file_name, is_master)
+                               version_or_path, file_name, is_master)
+
+def GetMavenUrl(is_master):
+  return GetVersionDestination('http://storage.googleapis.com/', '', is_master)
 
 def Main():
   if not 'BUILDBOT_BUILDERNAME' in os.environ:
@@ -113,14 +125,13 @@
       print('Uploading %s to %s' % (tagged_jar, destination))
       utils.upload_file_to_cloud_storage(tagged_jar, destination)
       print('File available at: %s' % GetUrl(version, file_name, is_master))
-    # Upload extracted maven directory for easy testing in studio.
-    zip_ref = zipfile.ZipFile(utils.MAVEN_ZIP, 'r')
-    zip_ref.extractall(temp)
-    zip_ref.close()
-    utils.upload_dir_to_cloud_storage(
-        os.path.join(temp, 'com'),
-        GetUploadDestination(version, 'com', is_master))
-    print('Maven repo root available at: %s' % GetUrl(version, '', is_master))
+      if file == utils.R8_JAR:
+        # Upload R8 to a maven compatible location.
+        maven_dst = GetUploadDestination(utils.get_maven_path(version),
+                                         'r8-%s.jar' % version, is_master)
+        utils.upload_file_to_cloud_storage(tagged_jar, maven_dst)
+        print('Maven repo root available at: %s' % GetMavenUrl(is_master))
+
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/bisect.py b/tools/bisect.py
index c8d1501..74c880e 100755
--- a/tools/bisect.py
+++ b/tools/bisect.py
@@ -3,33 +3,8 @@
 # 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 gradle
-import os
-import subprocess
 import sys
-import utils
-
-JAR = os.path.join(utils.REPO_ROOT, 'build', 'libs', 'bisect.jar')
-
-def run(args, build, debug):
-  if build:
-    gradle.RunGradle(['bisect'])
-  cmd = ['java']
-  if debug:
-    cmd.append('-ea')
-  cmd.extend(['-jar', JAR])
-  cmd.extend(args)
-  subprocess.check_call(cmd)
-
-def main():
-  build = True
-  args = []
-  for arg in sys.argv[1:]:
-    if arg in ("--build", "--no-build"):
-      build = arg == "--build"
-    else:
-      args.append(arg)
-  run(args, build, True)
+import toolhelper
 
 if __name__ == '__main__':
-  sys.exit(main())
+  sys.exit(toolhelper.run('bisect', sys.argv[1:]))
diff --git a/tools/compatdx.py b/tools/compatdx.py
index 25a59db..c4cb320 100755
--- a/tools/compatdx.py
+++ b/tools/compatdx.py
@@ -3,42 +3,8 @@
 # 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 gradle
-import os
-import subprocess
 import sys
-import utils
-
-def run(args, build = True, debug = True, profile = False, track_memory_file=None):
-  if build:
-    gradle.RunGradle(['CompatDX'])
-  cmd = []
-  if track_memory_file:
-    cmd.extend(['tools/track_memory.sh', track_memory_file])
-  cmd.append('java')
-  if debug:
-    cmd.append('-ea')
-  if profile:
-    cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
-  cmd.extend(['-jar', utils.COMPATDX_JAR])
-  cmd.extend(args)
-  subprocess.check_call(cmd)
-
-def main():
-  build = True
-  args = []
-  for arg in sys.argv[1:]:
-    if arg in ("--build", "--no-build"):
-      build = arg == "--build"
-    else:
-      args.append(arg)
-  try:
-    run(args, build)
-  except subprocess.CalledProcessError as e:
-    # In case anything relevant was printed to stdout, normally this is already
-    # on stderr.
-    print(e.output)
-    return e.returncode
+import toolhelper
 
 if __name__ == '__main__':
-  sys.exit(main())
+  sys.exit(toolhelper.run('compatdx', sys.argv[1:]))
diff --git a/tools/compatproguard.py b/tools/compatproguard.py
new file mode 100755
index 0000000..10542e3
--- /dev/null
+++ b/tools/compatproguard.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('compatproguard', sys.argv[1:]))
diff --git a/tools/create_maven_release.py b/tools/create_maven_release.py
index 0a7f122..662d53e 100755
--- a/tools/create_maven_release.py
+++ b/tools/create_maven_release.py
@@ -210,7 +210,7 @@
   # Create directory structure for this version.
   version = determine_version()
   with utils.TempDir() as tmp_dir:
-    version_dir = join(tmp_dir, 'com', 'android', 'tools', 'r8', version)
+    version_dir = join(tmp_dir, utils.get_maven_path(version))
     makedirs(version_dir)
     # Write the pom file.
     pom_file = join(version_dir, 'r8-' + version + '.pom')
diff --git a/tools/d8.py b/tools/d8.py
index bf97845..18a4a67 100755
--- a/tools/d8.py
+++ b/tools/d8.py
@@ -3,46 +3,8 @@
 # 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 gradle
-import os
-import subprocess
 import sys
-import utils
-
-def run(args, build=True, debug=True, profile=False,
-        track_memory_file=None):
-  if build:
-    gradle.RunGradle(['D8'])
-  cmd = []
-  if track_memory_file:
-    cmd.extend(['tools/track_memory.sh', track_memory_file])
-  cmd.append('java')
-  if debug:
-    cmd.append('-ea')
-  if profile:
-    cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
-  cmd.extend(['-jar', utils.D8_JAR])
-  cmd.extend(args)
-  utils.PrintCmd(cmd)
-  result = subprocess.check_output(cmd)
-  print(result)
-  return result
-
-def main():
-  build = True
-  args = []
-  for arg in sys.argv[1:]:
-    if arg in ("--build", "--no-build"):
-      build = arg == "--build"
-    else:
-      args.append(arg)
-  try:
-    run(args, build)
-  except subprocess.CalledProcessError as e:
-    # In case anything relevant was printed to stdout, normally this is already
-    # on stderr.
-    print(e.output)
-    return e.returncode
+import toolhelper
 
 if __name__ == '__main__':
-  sys.exit(main())
+  sys.exit(toolhelper.run('d8', sys.argv[1:]))
diff --git a/tools/d8logger.py b/tools/d8logger.py
new file mode 100755
index 0000000..9fb3508
--- /dev/null
+++ b/tools/d8logger.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('d8logger', sys.argv[1:]))
diff --git a/tools/dexfilemerger.py b/tools/dexfilemerger.py
new file mode 100755
index 0000000..7bdfc22
--- /dev/null
+++ b/tools/dexfilemerger.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('dexfilemerger', sys.argv[1:]))
diff --git a/tools/dexsegments.py b/tools/dexsegments.py
new file mode 100755
index 0000000..f984063
--- /dev/null
+++ b/tools/dexsegments.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('dexsegments', sys.argv[1:]))
diff --git a/tools/dexsplitter.py b/tools/dexsplitter.py
new file mode 100755
index 0000000..415b149
--- /dev/null
+++ b/tools/dexsplitter.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('dexsplitter', sys.argv[1:]))
diff --git a/tools/disasm.py b/tools/disasm.py
new file mode 100755
index 0000000..0d2599a
--- /dev/null
+++ b/tools/disasm.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('disasm', sys.argv[1:]))
diff --git a/tools/extractmarker.py b/tools/extractmarker.py
new file mode 100755
index 0000000..36a9c88
--- /dev/null
+++ b/tools/extractmarker.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('extractmarker', sys.argv[1:]))
diff --git a/tools/golem_build.py b/tools/golem_build.py
new file mode 100755
index 0000000..6d3a9b7
--- /dev/null
+++ b/tools/golem_build.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+# Utility script to make it easier to update what golem builds.
+
+import gradle
+import sys
+
+GRADLE_ARGS = ['--no-daemon']
+BUILD_TARGETS = ['R8', 'D8', 'buildExampleJars', 'CompatDx',
+                 'downloadAndroidCts', 'downloadDx']
+
+def Main():
+  gradle.RunGradle(GRADLE_ARGS + BUILD_TARGETS)
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/jardiff.py b/tools/jardiff.py
new file mode 100755
index 0000000..894131c
--- /dev/null
+++ b/tools/jardiff.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('jardiff', sys.argv[1:]))
diff --git a/tools/maindex.py b/tools/maindex.py
new file mode 100755
index 0000000..ff5329a
--- /dev/null
+++ b/tools/maindex.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import sys
+import toolhelper
+
+if __name__ == '__main__':
+  sys.exit(toolhelper.run('maindex', sys.argv[1:]))
diff --git a/tools/minify_tool.py b/tools/minify_tool.py
new file mode 100755
index 0000000..08b2ada
--- /dev/null
+++ b/tools/minify_tool.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+'''
+Run R8 (with the class-file backend) to optimize a command-line program.
+
+Given an input JAR (default: r8.jar) and a main-class, generates a new input JAR
+with the given main-class in the manifest along with a -keep rule for keeping
+just the main entrypoint, and runs R8 in release+classfile mode on the JAR.
+'''
+
+import argparse
+import os
+import re
+import sys
+import time
+import toolhelper
+import utils
+import zipfile
+
+KEEP = '-keep public class %s { public static void main(...); }\n'
+MANIFEST_PATH = 'META-INF/MANIFEST.MF'
+MANIFEST = 'Manifest-Version: 1.0\nMain-Class: %s\n\n'
+MANIFEST_PATTERN = r'Main-Class:\s*(\S+)'
+RT = os.path.join(utils.REPO_ROOT, 'third_party/openjdk/openjdk-rt-1.8/rt.jar')
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+                                 formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument(
+    '-i', '--input-jar', default=utils.R8_JAR,
+    help='Input JAR to use (default: build/libs/r8.jar)')
+parser.add_argument(
+    '-o', '--output-jar',
+    help='Path to output JAR (default: build/libs/<MainClass>-min.jar)')
+parser.add_argument(
+    '-l', '--lib', default=RT,
+    help='Path to rt.jar to use instead of OpenJDK 1.8')
+parser.add_argument(
+    '-m', '--mainclass',
+    help='Create/overwrite MANIFEST.MF with the given Main-Class')
+parser.add_argument(
+    '-O', '--no-debug', dest='debug', action='store_false',
+    help='Disable assertions when running R8')
+parser.add_argument(
+    '--print-runtimeraw', metavar='BENCHMARKNAME',
+    help='Print "<BENCHMARKNAME>(RunTimeRaw): <elapsed> ms" at the end')
+
+def generate_output_name(input_jar, mainclass):
+  if not mainclass:
+    input_base, input_ext = os.path.splitext(input_jar)
+    return '%s-min%s' % (input_base, input_ext)
+  base = mainclass[mainclass.rindex('.')+1:] if '.' in mainclass else mainclass
+  return os.path.join(utils.LIBS, '%s-min.jar' % base)
+
+def repackage(input_jar, output_jar, mainclass):
+  print("Repackaging %s to %s with Main-Class: %s..." %
+        (input_jar, output_jar, mainclass))
+  manifest = MANIFEST % mainclass
+  with zipfile.ZipFile(input_jar, 'r') as input_zf:
+    with zipfile.ZipFile(output_jar, 'w') as output_zf:
+      for zipinfo in input_zf.infolist():
+        if zipinfo.filename.upper() == MANIFEST_PATH:
+          assert manifest is not None
+          output_zf.writestr(MANIFEST_PATH, manifest)
+          manifest = None
+        else:
+          output_zf.writestr(zipinfo, input_zf.read(zipinfo))
+      if manifest is not None:
+        output_zf.writestr(MANIFEST_PATH, manifest)
+
+def extract_mainclass(input_jar):
+  with zipfile.ZipFile(input_jar, 'r') as input_zf:
+    try:
+      manifest = input_zf.getinfo(MANIFEST_PATH)
+    except KeyError:
+      raise SystemExit('No --mainclass specified and no manifest in input JAR.')
+    mo = re.search(MANIFEST_PATTERN, input_zf.read(manifest))
+    if not mo:
+      raise SystemExit(
+          'No --mainclass specified and no Main-Class in input JAR manifest.')
+    return mo.group(1)
+
+def minify_tool(mainclass=None, input_jar=utils.R8_JAR, output_jar=None, lib=RT,
+                debug=True, build=True, print_runtimeraw=None):
+  if output_jar is None:
+    output_jar = generate_output_name(input_jar, mainclass)
+  with utils.TempDir() as path:
+    if mainclass:
+      tmp_input_path = os.path.join(path, 'input.jar')
+      repackage(input_jar, tmp_input_path, mainclass)
+    else:
+      tmp_input_path = input_jar
+      mainclass = extract_mainclass(input_jar)
+    keep_path = os.path.join(path, 'keep.txt')
+    with open(keep_path, 'w') as fp:
+      fp.write(KEEP % mainclass)
+    args = ('--lib', lib,
+            '--classfile',
+            '--output', output_jar,
+            '--pg-conf', keep_path,
+            '--release',
+            tmp_input_path)
+    start_time = time.time()
+    return_code = toolhelper.run('r8', args, debug=debug, build=build)
+    if print_runtimeraw:
+      elapsed_ms = 1000 * (time.time() - start_time)
+      print('%s-Total(RunTimeRaw): %s ms' % (print_runtimeraw, elapsed_ms))
+    return return_code
+
+if __name__ == '__main__':
+  sys.exit(minify_tool(**vars(parser.parse_args())))
diff --git a/tools/r8.py b/tools/r8.py
index 3ae4da7..60c60a0 100755
--- a/tools/r8.py
+++ b/tools/r8.py
@@ -3,46 +3,8 @@
 # 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 gradle
-import os
-import subprocess
 import sys
-import utils
-
-def run(args, build=True, debug=True, profile=False,
-        track_memory_file=None):
-  if build:
-    gradle.RunGradle(['r8'])
-  cmd = []
-  if track_memory_file:
-    cmd.extend(['tools/track_memory.sh', track_memory_file])
-  cmd.append('java')
-  if debug:
-    cmd.append('-ea')
-  if profile:
-    cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
-  cmd.extend(['-jar', utils.R8_JAR])
-  cmd.extend(args)
-  utils.PrintCmd(cmd)
-  result = subprocess.check_output(cmd)
-  print(result)
-  return result
-
-def main():
-  build = True
-  args = []
-  for arg in sys.argv[1:]:
-    if arg in ("--build", "--no-build"):
-      build = arg == "--build"
-    else:
-      args.append(arg)
-  try:
-    run(args, build)
-  except subprocess.CalledProcessError as e:
-    # In case anything relevant was printed to stdout, normally this is already
-    # on stderr.
-    print(e.output)
-    return e.returncode
+import toolhelper
 
 if __name__ == '__main__':
-  sys.exit(main())
+  sys.exit(toolhelper.run('r8', sys.argv[1:]))
diff --git a/tools/run-d8-on-gmscore.py b/tools/run-d8-on-gmscore.py
index 63865d0..4a64693 100755
--- a/tools/run-d8-on-gmscore.py
+++ b/tools/run-d8-on-gmscore.py
@@ -3,11 +3,11 @@
 # for details. All rights reserved. Use of this source code is governed by a
 # BSD-style license that can be found in the LICENSE file.
 
-import d8
 import gmscore_data
 import optparse
 import os
 import sys
+import toolhelper
 
 def ParseOptions():
   result = optparse.OptionParser()
@@ -70,8 +70,13 @@
     with open(options.dump_args_file, 'w') as args_file:
       args_file.writelines([arg + os.linesep for arg in args])
   else:
-    d8.run(args, not options.no_build, not options.no_debug, options.profile,
-           options.track_memory_to_file)
+    toolhelper.run(
+        'd8',
+        args,
+        build=not options.no_build,
+        debug=not options.no_debug,
+        profile=options.profile,
+        track_memory_file=options.track_memory_to_file)
 
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/tools/run_bootstrap_benchmark.py b/tools/run_bootstrap_benchmark.py
new file mode 100755
index 0000000..0c608f9
--- /dev/null
+++ b/tools/run_bootstrap_benchmark.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import argparse
+import minify_tool
+import os
+import sys
+import utils
+
+
+PINNED_R8_JAR = os.path.join(utils.REPO_ROOT, 'third_party/r8/r8.jar')
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    '--print-runtimeraw', metavar='BENCHMARKNAME',
+    help='Print "<BENCHMARKNAME>(RunTimeRaw): <elapsed> ms" at the end')
+
+
+if __name__ == '__main__':
+  sys.exit(minify_tool.minify_tool(input_jar=PINNED_R8_JAR, debug=False,
+                                   build=False, **vars(parser.parse_args())))
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index b9bcc60..41340c7 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -10,10 +10,9 @@
 import sys
 import time
 
-import d8
 import gmail_data
 import gmscore_data
-import r8
+import toolhelper
 import utils
 import youtube_data
 
@@ -191,22 +190,20 @@
       if options.print_memoryuse and not options.track_memory_to_file:
         options.track_memory_to_file = os.path.join(temp,
             utils.MEMORY_USE_TMP_FILE)
-      if options.compiler == 'd8':
-        d8.run(args, not options.no_build, not options.no_debug,
-            options.profile, options.track_memory_to_file)
-      else:
-        if app_provided_pg_conf:
-          # Ensure that output of -printmapping and -printseeds go to the output
-          # location and not where the app Proguard configuration places them.
-          if outdir.endswith('.zip') or outdir.endswith('.jar'):
-            pg_outdir = os.path.dirname(outdir)
-          else:
-            pg_outdir = outdir
-          additional_pg_conf = GenerateAdditionalProguardConfiguration(
-              temp, os.path.abspath(pg_outdir))
-          args.extend(['--pg-conf', additional_pg_conf])
-        r8.run(args, not options.no_build, not options.no_debug,
-            options.profile, options.track_memory_to_file)
+      if options.compiler == 'r8' and app_provided_pg_conf:
+        # Ensure that output of -printmapping and -printseeds go to the output
+        # location and not where the app Proguard configuration places them.
+        if outdir.endswith('.zip') or outdir.endswith('.jar'):
+          pg_outdir = os.path.dirname(outdir)
+        else:
+          pg_outdir = outdir
+        additional_pg_conf = GenerateAdditionalProguardConfiguration(
+            temp, os.path.abspath(pg_outdir))
+        args.extend(['--pg-conf', additional_pg_conf])
+      toolhelper.run(options.compiler, args, build=not options.no_build,
+                     debug=not options.no_debug,
+                     profile=options.profile,
+                     track_memory_file=options.track_memory_to_file)
       if options.print_memoryuse:
         print('{}(MemoryUse): {}'
             .format(options.print_memoryuse,
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
new file mode 100644
index 0000000..a7f509c
--- /dev/null
+++ b/tools/toolhelper.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import gradle
+import os
+import subprocess
+import sys
+import utils
+
+def run(tool, args, build=None, debug=True,
+        profile=False, track_memory_file=None):
+  if build is None:
+    build, args = extract_build_from_args(args)
+  if build:
+    gradle.RunGradle(['r8'])
+  cmd = []
+  if track_memory_file:
+    cmd.extend(['tools/track_memory.sh', track_memory_file])
+  cmd.append('java')
+  if debug:
+    cmd.append('-ea')
+  if profile:
+    cmd.append('-agentlib:hprof=cpu=samples,interval=1,depth=8')
+  cmd.extend(['-jar', utils.R8_JAR, tool])
+  cmd.extend(args)
+  utils.PrintCmd(cmd)
+  return subprocess.call(cmd)
+
+def extract_build_from_args(input_args):
+  build = True
+  args = []
+  for arg in input_args:
+    if arg in ("--build", "--no-build"):
+      build = arg == "--build"
+    else:
+      args.append(arg)
+  return build, args
diff --git a/tools/utils.py b/tools/utils.py
index a138461..9f6959f 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -15,8 +15,6 @@
 TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
 REPO_ROOT = os.path.realpath(os.path.join(TOOLS_DIR, '..'))
 MEMORY_USE_TMP_FILE = 'memory_use.tmp'
-DEX_SEGMENTS_JAR = os.path.join(REPO_ROOT, 'build', 'libs',
-    'dexsegments.jar')
 DEX_SEGMENTS_RESULT_PATTERN = re.compile('- ([^:]+): ([0-9]+)')
 BUILD = os.path.join(REPO_ROOT, 'build')
 LIBS = os.path.join(BUILD, 'libs')
@@ -189,7 +187,7 @@
 # Return a dictionary: {segment_name -> segments_size}
 def getDexSegmentSizes(dex_files):
   assert len(dex_files) > 0
-  cmd = ['java', '-jar', DEX_SEGMENTS_JAR]
+  cmd = ['java', '-jar', R8_JAR, 'dexsegments']
   cmd.extend(dex_files)
   PrintCmd(cmd)
   output = subprocess.check_output(cmd)
@@ -207,6 +205,9 @@
 
   return result
 
+def get_maven_path(version):
+  return os.path.join('com', 'android', 'tools', 'r8', version)
+
 def print_dexsegments(prefix, dex_files):
   for segment_name, size in getDexSegmentSizes(dex_files).items():
     print('{}-{}(CodeSize): {}'