Merge "Add test for signature attribute parsing"
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
new file mode 100644
index 0000000..b1bc7ca
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.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.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
+import com.android.tools.r8.utils.ExceptionUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public class PrintSeeds {
+
+  private static final String USAGE =
+      "Arguments: <rt.jar> <r8.jar> <pg-conf.txt>\n"
+          + "\n"
+          + "PrintSeeds prints the classes, interfaces, methods and fields selected by\n"
+          + "<pg-conf.txt> when compiling <r8.jar> alongside <rt.jar>.";
+
+  public static void main(String[] args) throws Exception {
+    if (args.length != 3) {
+      System.out.println(USAGE);
+      System.exit(1);
+    }
+    Path rtJar = Paths.get(args[0]);
+    Path r8Jar = Paths.get(args[1]);
+    Path pgConf = Paths.get(args[2]);
+    R8Command command =
+        R8Command.builder()
+            .addLibraryFiles(rtJar)
+            .addProgramFiles(r8Jar)
+            .addProguardConfigurationFiles(pgConf)
+            .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+            .build();
+    Set<String> descriptors = new ArchiveClassFileProvider(r8Jar).getClassDescriptors();
+    InternalOptions options = command.getInternalOptions();
+    ExecutorService executorService = ThreadUtils.getExecutorService(options);
+    ExceptionUtils.withR8CompilationHandler(
+        command.getReporter(),
+        () -> {
+          try {
+            run(command, descriptors, options, executorService);
+          } finally {
+            executorService.shutdown();
+          }
+        });
+  }
+
+  private static void run(
+      R8Command command, Set<String> descriptors, InternalOptions options, ExecutorService executor)
+      throws IOException {
+    Timing timing = new Timing("PrintSeeds");
+    try {
+      DexApplication application =
+          new ApplicationReader(command.getInputApp(), options, timing).read(executor).toDirect();
+      AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
+      RootSet rootSet =
+          new RootSetBuilder(
+                  appInfo, application, options.proguardConfiguration.getRules(), options)
+              .run(executor);
+      Enqueuer enqueuer = new Enqueuer(appInfo, options, false);
+      appInfo = enqueuer.traceApplication(rootSet, executor, timing);
+      RootSetBuilder.writeSeeds(
+          appInfo.withLiveness(),
+          System.out,
+          type -> descriptors.contains(type.toDescriptorString()));
+    } catch (ExecutionException e) {
+      R8.unwrapExecutionException(e);
+      throw new AssertionError(e); // unwrapping method should have thrown
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
new file mode 100644
index 0000000..0e69de6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -0,0 +1,307 @@
+// 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.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfo.ResolutionResult;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class PrintUses {
+
+  private static final String USAGE =
+      "Arguments: <rt.jar> <r8.jar> <sample.jar>\n"
+          + "\n"
+          + "PrintUses prints the classes, interfaces, methods and fields used by <sample.jar>,\n"
+          + "restricted to classes and interfaces in <r8.jar> that are not in <sample.jar>.\n"
+          + "<rt.jar> and <r8.jar> should point to libraries used by <sample.jar>.";
+
+  private final Set<String> descriptors;
+  private final PrintStream out;
+  private Set<DexType> types = Sets.newIdentityHashSet();
+  private Map<DexType, Set<DexMethod>> methods = Maps.newIdentityHashMap();
+  private Map<DexType, Set<DexField>> fields = Maps.newIdentityHashMap();
+  private final DexApplication application;
+  private final AppInfoWithSubtyping appInfo;
+  private int errors;
+
+  class UseCollector extends UseRegistry {
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      DexEncodedMethod target = appInfo.lookupVirtualTarget(method.holder, method);
+      if (target != null && target.method != method) {
+        addType(method.holder);
+        addMethod(target.method);
+      } else {
+        addMethod(method);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      addMethod(method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      DexEncodedMethod target = appInfo.lookupStaticTarget(method);
+      if (target != null && target.method != method) {
+        addType(method.holder);
+        addMethod(target.method);
+      } else {
+        addMethod(method);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return registerInvokeVirtual(method);
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      addMethod(method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      addType(type);
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      addField(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      addType(type);
+      return false;
+    }
+
+    private void addType(DexType type) {
+      if (isTargetType(type) && types.add(type)) {
+        methods.put(type, Sets.newIdentityHashSet());
+        fields.put(type, Sets.newIdentityHashSet());
+      }
+    }
+
+    private boolean isTargetType(DexType type) {
+      return descriptors.contains(type.toDescriptorString());
+    }
+
+    private void addField(DexField field) {
+      addType(field.type);
+      addType(field.clazz);
+      Set<DexField> typeFields = fields.get(field.clazz);
+      if (typeFields != null) {
+        typeFields.add(field);
+      }
+    }
+
+    private void addMethod(DexMethod method) {
+      addType(method.holder);
+      for (DexType parameterType : method.proto.parameters.values) {
+        addType(parameterType);
+      }
+      addType(method.proto.returnType);
+      Set<DexMethod> typeMethods = methods.get(method.holder);
+      if (typeMethods != null) {
+        typeMethods.add(method);
+      }
+    }
+
+    private void registerField(DexEncodedField field) {
+      registerTypeReference(field.field.type);
+    }
+
+    private void registerMethod(DexEncodedMethod method) {
+      DexEncodedMethod superTarget = appInfo.lookupSuperTarget(method.method, method.method.holder);
+      if (superTarget != null) {
+        registerInvokeSuper(superTarget.method);
+      }
+      for (DexType type : method.method.proto.parameters.values) {
+        registerTypeReference(type);
+      }
+      registerTypeReference(method.method.proto.returnType);
+      method.registerCodeReferences(this);
+    }
+
+    private void registerSuperType(DexProgramClass clazz, DexType superType) {
+      registerTypeReference(superType);
+      // If clazz overrides any methods in superType, we should keep those as well.
+      clazz.forEachMethod(
+          method -> {
+            ResolutionResult resolutionResult = appInfo.resolveMethod(superType, method.method);
+            for (DexEncodedMethod dexEncodedMethod : resolutionResult.asListOfTargets()) {
+              addMethod(dexEncodedMethod.method);
+            }
+          });
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    if (args.length != 3) {
+      System.out.println(USAGE);
+      return;
+    }
+    AndroidApp.Builder builder = AndroidApp.builder();
+    Path rtJar = Paths.get(args[0]);
+    builder.addLibraryFile(rtJar);
+    Path r8Jar = Paths.get(args[1]);
+    builder.addLibraryFile(r8Jar);
+    Path sampleJar = Paths.get(args[2]);
+    builder.addProgramFile(sampleJar);
+    Set<String> descriptors = new HashSet<>(getDescriptors(r8Jar));
+    descriptors.removeAll(getDescriptors(sampleJar));
+    PrintUses printUses = new PrintUses(descriptors, builder.build(), System.out);
+    printUses.analyze();
+    printUses.print();
+    if (printUses.errors > 0) {
+      System.err.println(printUses.errors + " errors");
+      System.exit(1);
+    }
+  }
+
+  private static Set<String> getDescriptors(Path path) throws IOException {
+    return new ArchiveClassFileProvider(path).getClassDescriptors();
+  }
+
+  private PrintUses(Set<String> descriptors, AndroidApp inputApp, PrintStream out)
+      throws Exception {
+    this.descriptors = descriptors;
+    this.out = out;
+    InternalOptions options = new InternalOptions();
+    application =
+        new ApplicationReader(inputApp, options, new Timing("PrintUses")).read().toDirect();
+    appInfo = new AppInfoWithSubtyping(application);
+  }
+
+  private void analyze() {
+    UseCollector useCollector = new UseCollector();
+    for (DexProgramClass dexProgramClass : application.classes()) {
+      useCollector.registerSuperType(dexProgramClass, dexProgramClass.superType);
+      for (DexType implementsType : dexProgramClass.interfaces.values) {
+        useCollector.registerSuperType(dexProgramClass, implementsType);
+      }
+      dexProgramClass.forEachMethod(useCollector::registerMethod);
+      dexProgramClass.forEachField(useCollector::registerField);
+    }
+  }
+
+  private void print() {
+    List<DexType> types = new ArrayList<>(this.types);
+    types.sort(Comparator.comparing(DexType::toSourceString));
+    for (DexType type : types) {
+      String typeName = type.toSourceString();
+      DexClass dexClass = application.definitionFor(type);
+      if (dexClass == null) {
+        error("Could not find definition for type " + type.toSourceString());
+        continue;
+      }
+      out.println(typeName);
+      List<DexMethod> methods = new ArrayList<>(this.methods.get(type));
+      List<String> methodDefinitions = new ArrayList<>(methods.size());
+      for (DexMethod method : methods) {
+        DexEncodedMethod encodedMethod = dexClass.lookupMethod(method);
+        if (encodedMethod == null) {
+          error("Could not find definition for method " + method.toSourceString());
+          continue;
+        }
+        methodDefinitions.add(getMethodSourceString(encodedMethod));
+      }
+      methodDefinitions.sort(Comparator.naturalOrder());
+      for (String encodedMethod : methodDefinitions) {
+        out.println(typeName + ": " + encodedMethod);
+      }
+      List<DexField> fields = new ArrayList<>(this.fields.get(type));
+      fields.sort(Comparator.comparing(DexField::toSourceString));
+      for (DexField field : fields) {
+        out.println(
+            typeName + ": " + field.type.toSourceString() + " " + field.name.toSourceString());
+      }
+    }
+  }
+
+  private void error(String message) {
+    out.println("# Error: " + message);
+    errors += 1;
+  }
+
+  private static String getMethodSourceString(DexEncodedMethod encodedMethod) {
+    DexMethod method = encodedMethod.method;
+    StringBuilder builder = new StringBuilder();
+    if (encodedMethod.accessFlags.isConstructor()) {
+      if (encodedMethod.accessFlags.isStatic()) {
+        builder.append("<clinit>");
+      } else {
+        String holderName = method.holder.toSourceString();
+        String constructorName = holderName.substring(holderName.lastIndexOf('.') + 1);
+        builder.append(constructorName);
+      }
+    } else {
+      builder
+          .append(method.proto.returnType.toSourceString())
+          .append(" ")
+          .append(method.name.toSourceString());
+    }
+    builder.append("(");
+    for (int i = 0; i < method.getArity(); i++) {
+      if (i != 0) {
+        builder.append(",");
+      }
+      builder.append(method.proto.parameters.values[i].toSourceString());
+    }
+    builder.append(")");
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 077e220..e42d8b0 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -287,7 +287,7 @@
         if (options.proguardConfiguration.isPrintSeeds()) {
           ByteArrayOutputStream bytes = new ByteArrayOutputStream();
           PrintStream out = new PrintStream(bytes);
-          RootSetBuilder.writeSeeds(appInfo.withLiveness(), out);
+          RootSetBuilder.writeSeeds(appInfo.withLiveness(), out, type -> true);
           out.flush();
           proguardSeedsData = bytes.toString();
         }
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 0e35847..28e1f93 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -28,38 +28,32 @@
       return;
     }
     switch (args[0]) {
-      case "r8":
-        R8.main(shift(args));
-        break;
-      case "d8":
-        D8.main(shift(args));
+      case "bisect":
+        Bisect.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 "d8":
+        D8.main(shift(args));
+        break;
       case "d8logger":
         D8Logger.main(shift(args));
         break;
-      case "disasm":
-        Disassemble.main(shift(args));
-        break;
-      case "bisect":
-        Bisect.main(shift(args));
+      case "dexfilemerger":
+        DexFileMerger.main(shift(args));
         break;
       case "dexsegments":
         DexSegments.main(shift(args));
         break;
-      case "maindex":
-        GenerateMainDexList.main(shift(args));
+      case "dexsplitter":
+        DexSplitter.main(shift(args));
+        break;
+      case "disasm":
+        Disassemble.main(shift(args));
         break;
       case "extractmarker":
         ExtractMarker.main(shift(args));
@@ -67,6 +61,18 @@
       case "jardiff":
         JarDiff.main(shift(args));
         break;
+      case "maindex":
+        GenerateMainDexList.main(shift(args));
+        break;
+      case "printseeds":
+        PrintSeeds.main(shift(args));
+        break;
+      case "printuses":
+        PrintUses.main(shift(args));
+        break;
+      case "r8":
+        R8.main(shift(args));
+        break;
       default:
         runDefault(args);
         break;
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 0ba40b3..fb3b067 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.20-dev";
+  public static final String LABEL = "1.2.21-dev";
 
   private Version() {
   }
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 015b8a3..0eed4a6 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
@@ -4,6 +4,7 @@
 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.graph.DexType;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.CfSourceCode;
@@ -52,6 +53,10 @@
 
   @Override
   public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
+    if (!builder.isGeneratingClassFiles()) {
+      // TODO(b/109789539): Implement this case (see JarSourceCode.buildPrelude()/buildPostlude()).
+      throw new Unimplemented("CfMultiANewArray to DEX backend");
+    }
     int[] dimensions = state.popReverse(this.dimensions);
     builder.addMultiNewArray(type, state.push(type).register, dimensions);
   }
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 ffa34d1..b8e0bd8 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -225,8 +225,11 @@
       ValueNumberGenerator generator,
       Position callerPosition,
       Origin origin) {
-    assert !options.isGeneratingDex() || !encodedMethod.accessFlags.isSynchronized()
-        : "Converting CfCode to IR not supported for DEX output of synchronized methods.";
+    // TODO(b/109789541): Implement CF->IR->DEX for synchronized methods.
+    if (options.isGeneratingDex() && encodedMethod.accessFlags.isSynchronized()) {
+      throw new Unimplemented(
+          "Converting CfCode to IR not supported for DEX output of synchronized methods.");
+    }
     CfSourceCode source = new CfSourceCode(this, encodedMethod, callerPosition, origin);
     IRBuilder builder =
         (generator == null)
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 454ed29..afc2969 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -533,7 +533,8 @@
           return MemberType.OBJECT;
         case Opcodes.BALOAD:
         case Opcodes.BASTORE:
-          return MemberType.BOOLEAN; // TODO: Distinguish byte and boolean.
+          // TODO(b/109788783): Distinguish byte and boolean.
+          return MemberType.BOOLEAN;
         case Opcodes.CALOAD:
         case Opcodes.CASTORE:
           return MemberType.CHAR;
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
index e68e3d5..ca79b0f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -324,7 +324,7 @@
     buildArgumentInstructions(builder);
     recordStateForTarget(0, state.getSnapshot());
     // TODO: addDebugLocalUninitialized + addDebugLocalStart for non-argument locals live at 0
-    // TODO: Generate method synchronization
+    // TODO(b/109789541): Generate method synchronization for DEX backend.
     inPrelude = false;
   }
 
@@ -353,7 +353,7 @@
 
   @Override
   public void buildPostlude(IRBuilder builder) {
-    // Since we're generating classfiles, we never need to synthesize monitor enter/exit.
+    // TODO(b/109789541): Generate method synchronization for DEX backend.
   }
 
   @Override
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 bd3442c..ae57f48 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
@@ -1707,10 +1707,8 @@
           return values;
         }
       }
-      block =
-          block.exit().isGoto() && !visitedBlocks.contains(block.exit().asGoto().getTarget())
-              ? block.exit().asGoto().getTarget()
-              : null;
+      BasicBlock nextBlock = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+      block = nextBlock != null && !visitedBlocks.contains(nextBlock) ? nextBlock : null;
       it = block != null ? block.listIterator() : null;
     } while (it != null);
     return null;
@@ -1801,7 +1799,9 @@
       // Second pass: remove all the array put instructions for the array for which we have
       // inserted a fill array data instruction instead.
       if (!storesToRemoveForArray.isEmpty()) {
+        Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet();
         do {
+          visitedBlocks.add(block);
           it = block.listIterator();
           while (it.hasNext()) {
             Instruction instruction = it.next();
@@ -1823,7 +1823,8 @@
               }
             }
           }
-          block = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+          BasicBlock nextBlock = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
+          block = nextBlock != null && !visitedBlocks.contains(nextBlock) ? nextBlock : null;
         } while (block != null);
       }
     }
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 085d9b3..51feb9d 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -734,6 +734,7 @@
   private void transitionDefaultMethodsForInstantiatedClass(DexType iface, DexType instantiatedType,
       ScopedDexMethodSet seen) {
     DexClass clazz = appInfo.definitionFor(iface);
+    assert clazz != null : "Missing class " + iface.toSourceString();
     assert clazz.accessFlags.isInterface();
     SetWithReason<DexEncodedMethod> reachableMethods = reachableVirtualMethods.get(iface);
     if (reachableMethods != null) {
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 700e146..c860d83 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -43,6 +43,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 public class RootSetBuilder {
@@ -441,17 +442,28 @@
   }
 
   // TODO(67934426): Test this code.
-  public static void writeSeeds(AppInfoWithLiveness appInfo, PrintStream out) {
+  public static void writeSeeds(
+      AppInfoWithLiveness appInfo, PrintStream out, Predicate<DexType> include) {
     for (DexItem seed : appInfo.getPinnedItems()) {
       if (seed instanceof DexType) {
-        out.println(seed.toSourceString());
+        if (include.test((DexType) seed)) {
+          out.println(seed.toSourceString());
+        }
       } else if (seed instanceof DexField) {
         DexField field = ((DexField) seed);
-        out.println(
-            field.clazz.toSourceString() + ": " + field.type.toSourceString() + " " + field.name
-                .toSourceString());
+        if (include.test(field.clazz)) {
+          out.println(
+              field.clazz.toSourceString()
+                  + ": "
+                  + field.type.toSourceString()
+                  + " "
+                  + field.name.toSourceString());
+        }
       } else if (seed instanceof DexMethod) {
         DexMethod method = (DexMethod) seed;
+        if (!include.test(method.holder)) {
+          continue;
+        }
         out.print(method.holder.toSourceString() + ": ");
         DexEncodedMethod encodedMethod = appInfo.definitionFor(method);
         if (encodedMethod.accessFlags.isConstructor()) {
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 37aa208..499ba47 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -54,17 +54,18 @@
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
         try (InputStream stream = zipFile.getInputStream(entry)) {
-          Path name = Paths.get(entry.getName());
-          Origin entryOrigin = new ArchiveEntryOrigin(entry.getName(), origin);
-          if (archive.matchesFile(name)) {
-            if (isDexFile(name)) {
+          String name = entry.getName();
+          Path path = Paths.get(name);
+          Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
+          if (archive.matchesFile(path)) {
+            if (isDexFile(path)) {
               if (!ignoreDexInArchive) {
                 ProgramResource resource =
                     OneShotByteResource.create(
                         Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
                 dexResources.add(resource);
               }
-            } else if (isClassFile(name)) {
+            } else if (isClassFile(path)) {
               String descriptor = DescriptorUtils.guessTypeDescriptor(name);
               ProgramResource resource =
                   OneShotByteResource.create(
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index fa045e6..a846ea7 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -341,22 +341,35 @@
     }
   }
 
-  // Guess class descriptor from location of the class file.
+  /**
+   * Guess class descriptor from location of the class file on the file system
+   *
+   * @param name Path of the file to convert to the corresponding descriptor
+   * @return java class descriptor
+   */
   public static String guessTypeDescriptor(Path name) {
-    return guessTypeDescriptor(name.toString());
+    String fileName = name.toString();
+    if (File.separatorChar != '/') {
+      fileName = fileName.replace(File.separatorChar, '/');
+    }
+    return guessTypeDescriptor(fileName);
   }
 
-  // Guess class descriptor from location of the class file.
+  /**
+   * Guess class descriptor from location of the class file. This method assumes that the
+   * name uses '/' as the separator. Therefore, this should not be the name of a file
+   * on a file system.
+   *
+   * @param name the location of the class file to convert to descriptor
+   * @return java class descriptor
+   */
   public static String guessTypeDescriptor(String name) {
     assert name != null;
     assert name.endsWith(CLASS_EXTENSION) :
         "Name " + name + " must have " + CLASS_EXTENSION + " suffix";
-    String fileName =
-        File.separatorChar == '/' ? name.toString() :
-            name.toString().replace(File.separatorChar, '/');
-    String descriptor = fileName.substring(0, fileName.length() - CLASS_EXTENSION.length());
+    String descriptor = name.substring(0, name.length() - CLASS_EXTENSION.length());
     if (descriptor.contains(".")) {
-      throw new CompilationError("Unexpected class file name: " + fileName);
+      throw new CompilationError("Unexpected class file name: " + name);
     }
     return 'L' + descriptor + ';';
   }
diff --git a/src/test/examples/barray/BArray.java b/src/test/examples/barray/BArray.java
index 0ca6505..26ac5f8 100644
--- a/src/test/examples/barray/BArray.java
+++ b/src/test/examples/barray/BArray.java
@@ -6,20 +6,59 @@
 public class BArray {
 
   public static void main(String[] args) {
+    System.out.println("null boolean: " + readNullBooleanArray());
+    System.out.println("null byte: " + readNullByteArray());
+    System.out.println("boolean: " + readBooleanArray(writeBooleanArray(args)));
+    System.out.println("byte: " + readByteArray(writeByteArray(args)));
+  }
+
+  public static boolean readNullBooleanArray() {
     boolean[] boolArray = null;
+    try {
+      return boolArray[0] || boolArray[1];
+    } catch (Throwable e) {
+      return true;
+    }
+  }
+
+  public static byte readNullByteArray() {
     byte[] byteArray = null;
-    boolean bool;
-    byte bits;
     try {
-      bool = boolArray[0] || boolArray[1];
+      return byteArray[0];
     } catch (Throwable e) {
-      bool = true;
+      return 42;
     }
+  }
+
+  public static boolean[] writeBooleanArray(String[] args) {
+    boolean[] array = new boolean[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = args[i].length() == 42;
+    }
+    return array;
+  }
+
+  public static byte[] writeByteArray(String[] args) {
+    byte[] array = new byte[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = (byte) args[i].length();
+    }
+    return array;
+  }
+
+  public static boolean readBooleanArray(boolean[] boolArray) {
     try {
-      bits = byteArray[0];
+      return boolArray[0] || boolArray[1];
     } catch (Throwable e) {
-      bits = 42;
+      return true;
     }
-    System.out.println("bits " + bits + " and bool " + bool);
+  }
+
+  public static byte readByteArray(byte[] byteArray) {
+    try {
+      return byteArray[0];
+    } catch (Throwable e) {
+      return 42;
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 9cc2b48..8038c98 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -64,6 +64,10 @@
     Assert.assertEquals(javaResult.stdout, d8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8Result.stdout);
     Assert.assertEquals(javaResult.stdout, r8ShakenResult.stdout);
+    Assert.assertEquals(0, javaResult.exitCode);
+    Assert.assertEquals(0, d8Result.exitCode);
+    Assert.assertEquals(0, r8Result.exitCode);
+    Assert.assertEquals(0, r8ShakenResult.exitCode);
   }
 
   protected void ensureR8FailsWithCompilationError(String main, byte[]... classes)
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index fe5ae72..4afd0c2 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -148,6 +148,11 @@
     if (output == Output.CF && getFailingCompileCf().contains(mainClass)) {
       thrown.expect(Throwable.class);
     }
+    if (frontend == Frontend.CF
+        && output == Output.DEX
+        && getFailingCompileCfToDex().contains(mainClass)) {
+      thrown.expect(Throwable.class);
+    }
     OutputMode outputMode = output == Output.CF ? OutputMode.ClassFile : OutputMode.DexIndexed;
     switch (compiler) {
       case D8: {
@@ -215,6 +220,12 @@
       thrown = ExpectedException.none();
     }
 
+    if (frontend == Frontend.CF
+        && output == Output.DEX
+        && getFailingRunCfToDex().contains(mainClass)) {
+      thrown.expect(Throwable.class);
+    }
+
     if (output == Output.CF) {
       ToolHelper.ProcessResult result = ToolHelper.runJava(generated, mainClass);
       if (result.exitCode != 0) {
@@ -257,6 +268,11 @@
 
   protected abstract Map<String, TestCondition> getFailingRunCf();
 
+  protected abstract Set<String> getFailingCompileCfToDex();
+
+  // TODO(mathiasr): Add CompilerSet for CfToDex so we can fold this into getFailingRun().
+  protected abstract Set<String> getFailingRunCfToDex();
+
   protected abstract Set<String> getFailingCompileCf();
 
   protected abstract Set<String> getFailingOutputCf();
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
index 97c8624..dc94a71 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
@@ -49,6 +49,16 @@
   }
 
   @Override
+  protected Set<String> getFailingCompileCfToDex() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  protected Set<String> getFailingRunCfToDex() {
+    return Collections.emptySet();
+  }
+
+  @Override
   protected Set<String> getFailingCompileCf() {
     return Collections.emptySet();
   }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index d8b886c..4ee928b 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -109,6 +109,14 @@
               test,
               Frontend.CF,
               Output.CF));
+      fullTestList.add(
+          makeTest(
+              Input.JAVAC_ALL,
+              CompilerUnderTest.R8,
+              CompilationMode.RELEASE,
+              test,
+              Frontend.CF,
+              Output.DEX));
     }
     return fullTestList;
   }
@@ -143,6 +151,29 @@
   }
 
   @Override
+  protected Set<String> getFailingCompileCfToDex() {
+    return new ImmutableSet.Builder<String>()
+        // TODO(b/109788783): Implement byte/boolean distinction for array load/store.
+        .add("arrayaccess.ArrayAccess")
+        .add("barray.BArray")
+        .add("filledarray.FilledArray")
+        .build();
+  }
+
+  @Override
+  protected Set<String> getFailingRunCfToDex() {
+    return new ImmutableSet.Builder<String>()
+        // TODO(b/109789541): Implement method synchronization for DEX backend.
+        .add("sync.Sync")
+        // TODO(b/109789539): Implement CfMultiANewArray.buildIR() for DEX backend.
+        .add("newarray.NewArray")
+        .add("trycatch.TryCatch")
+        .add("regress_70737019.Test")
+        .add("regress_72361252.Test")
+        .build();
+  }
+
+  @Override
   protected Set<String> getFailingCompileCf() {
     return new ImmutableSet.Builder<String>()
         .build();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java b/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java
new file mode 100644
index 0000000..3f7bcfb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/B87341268.java
@@ -0,0 +1,39 @@
+// 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 static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import org.junit.Test;
+
+public class B87341268 extends TestBase {
+  @Test
+  public void test() throws Exception {
+    AndroidApp app = compileWithD8(readClasses(TestClass.class));
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject clazz = inspector.clazz(TestClass.class);
+    assertThat(clazz, isPresent());
+  }
+}
+
+class TestClass {
+  int loop(String arg) {
+    long[] array = { 0L, 1L, 2L };
+    int length = -1;
+    while (true) {
+      try {
+        length = arg.length();
+      } catch (Exception e) {
+        System.err.println(e.getMessage());
+        break;
+      }
+    }
+    return length;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
index 8a6bdcf..bfb0674 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
@@ -231,21 +231,25 @@
       mv.visitJumpInsn(IFEQ, l7_b1);
       mv.visitLabel(l7_b0);
       mv.visitInsn(ACONST_NULL);
+      // At this point, NULL flows to l7_join.
       mv.visitJumpInsn(GOTO, l7_join);
       mv.visitLabel(l7_b1);
       mv.visitVarInsn(ALOAD, 10);
-      // Swap the new-instance or null, with the byte-array.
+      // At this point, an uninitialized String flows to l7_join.
       mv.visitLabel(l7_join);
+      // At this point, stack contains [staticByteArray, NULL/uninitialized]
       mv.visitInsn(SWAP);
-      // Load the new-instacne and swap again with the byte-array.
+      // At this point, stack contains [NULL/uninitialized, staticByteArray]
       mv.visitVarInsn(ALOAD, 10);
+      // At this point, stack contains [NULL/uninitialized, staticByteArray, uninitialized]
       mv.visitInsn(SWAP);
+      // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray]
       mv.visitInsn(ICONST_0);
-      // Invoke special will now always be on the new-instance as receiver.
+      // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray, 0]
       mv.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "<init>", "([BI)V", false);
-      // Return will be either new-instance or null.
+      // Just before ARETURN, stack contains [NULL/String].
       // This will force a non-trivial phi of the new-instance on this block, ie, prior to <init>.
-      // This phi must remain.
+      // This phi must remain since it is not trivial.
       mv.visitInsn(ARETURN);
       mv.visitLabel(l8);
       mv.visitVarInsn(ILOAD, 0);
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
index ff7da8f..ccf6467 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
@@ -31,13 +31,14 @@
 
   public static void compare(String output, int iterations) {
     String expected = "java.security.SecureRandom";
-    if (output.equals(expected)) {
+    if (expected.equals(output)) {
       return;
     }
     System.out.println(
         "After " + iterations + " iterations, expected \"" +
         expected + "\", but got \"" + output + "\"");
-    System.exit(1);
+    // Exit with code 0 to allow test to use ensureSameOutput().
+    System.exit(0);
   }
 
   public static void compareHash(int a, int b, byte[] c, int iterations) {
@@ -53,6 +54,7 @@
     System.out.println("staticIntB: " + b);
     System.out.print("staticIntByteArray: ");
     printByteArray(c);
-    System.exit(1);
+    // Exit with code 0 to allow test to use ensureSameOutput().
+    System.exit(0);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
index 3c7c465..3a56004 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
@@ -3,8 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.regress.b78493232;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
 import com.android.tools.r8.AsmTestBase;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.AndroidApp;
 import org.junit.Test;
 
 // Variant of Regress78493232, but where the new-instance is forced to flow to a non-trivial phi
@@ -15,6 +22,57 @@
   public void test() throws Exception {
     // Run test on JVM and ART(x86) to ensure expected behavior.
     // Running the same test on an ARM JIT causes errors.
+
+    // TODO(b/80118070): Remove this if-statement when fixed.
+    if (ToolHelper.getDexVm().getVersion() != Version.V5_1_1) {
+      AndroidApp app =
+          buildAndroidApp(
+              Regress78493232Dump_WithPhi.dump(),
+              ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+      ProcessResult javaResult =
+          runOnJava(
+              Regress78493232Dump_WithPhi.CLASS_NAME,
+              Regress78493232Dump_WithPhi.dump(),
+              ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+      ProcessResult d8Result =
+          runOnArtRaw(compileWithD8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+      ProcessResult r8Result =
+          runOnArtRaw(compileWithR8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+      String proguardConfig =
+          keepMainProguardConfiguration(Regress78493232Dump_WithPhi.CLASS_NAME)
+              + "-dontobfuscate\n";
+      ProcessResult r8ShakenResult =
+          runOnArtRaw(compileWithR8(app, proguardConfig), Regress78493232Dump_WithPhi.CLASS_NAME);
+      assertEquals(
+          "After 0 iterations, expected \"java.security.SecureRandom\", but got \"null\"\n",
+          javaResult.stdout);
+      assertEquals(0, javaResult.exitCode);
+      switch (ToolHelper.getDexVm().getVersion()) {
+        case V4_0_4:
+        case V4_4_4:
+        case V7_0_0:
+        case DEFAULT:
+          assertNotEquals(-1, d8Result.stderr.indexOf("java.lang.VerifyError"));
+          assertNotEquals(-1, r8Result.stderr.indexOf("java.lang.VerifyError"));
+          assertNotEquals(-1, r8ShakenResult.stderr.indexOf("java.lang.VerifyError"));
+          assertEquals(1, d8Result.exitCode);
+          assertEquals(1, r8Result.exitCode);
+          assertEquals(1, r8ShakenResult.exitCode);
+          break;
+        case V6_0_1:
+          assertEquals("Completed successfully after 1000 iterations\n", d8Result.stdout);
+          assertEquals("Completed successfully after 1000 iterations\n", r8Result.stdout);
+          assertEquals("Completed successfully after 1000 iterations\n", r8ShakenResult.stdout);
+          assertEquals(0, d8Result.exitCode);
+          assertEquals(0, r8Result.exitCode);
+          assertEquals(0, r8ShakenResult.exitCode);
+          break;
+        case V5_1_1:
+        default:
+          throw new Unreachable();
+      }
+      return;
+    }
     ensureSameOutput(
         Regress78493232Dump_WithPhi.CLASS_NAME,
         Regress78493232Dump_WithPhi.dump(),
diff --git a/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java b/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
index 58180e3..7ad9163 100644
--- a/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
+++ b/src/test/java/com/android/tools/r8/utils/DescriptorUtilsTest.java
@@ -6,7 +6,10 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.Test;
 
 public class DescriptorUtilsTest {
@@ -67,4 +70,16 @@
     assertEquals("java.lang.Object", DescriptorUtils.descriptorToJavaType("Ljava/lang/Object;"));
     assertEquals("a.b.C", DescriptorUtils.descriptorToJavaType("La/b/C;"));
   }
+
+  @Test
+  public void guessClassDescriptor() {
+    String obj = "java/lang/Object.class";
+    assertEquals("Ljava/lang/Object;", DescriptorUtils.guessTypeDescriptor(obj));
+    String objBackslash = "java\\lang\\Object.class";
+    assertEquals("Ljava\\lang\\Object;", DescriptorUtils.guessTypeDescriptor(objBackslash));
+    String objFileSeparatorChar =
+        "java" + File.separatorChar + "lang" + File.separatorChar + "Object.class";
+    assertEquals("Ljava/lang/Object;",
+        DescriptorUtils.guessTypeDescriptor(Paths.get(objFileSeparatorChar)));
+  }
 }
diff --git a/tools/api_sample_coverage.py b/tools/api_sample_coverage.py
new file mode 100755
index 0000000..b14ad1a
--- /dev/null
+++ b/tools/api_sample_coverage.py
@@ -0,0 +1,75 @@
+#!/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.
+
+'''
+Compare the R8 API used by the API usage sample to the API kept by @Keep.
+'''
+
+import argparse
+import os
+import subprocess
+import utils
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+                                 formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument('-o', '--output-dir')
+
+API_SAMPLE_JAR = 'tests/d8_api_usage_sample.jar'
+
+
+def main(output_dir=None):
+  if output_dir is None:
+    output_dir = ''
+
+  printseeds_path = os.path.join(output_dir, 'keep-seeds.txt')
+  printseeds_args = [
+    'java', '-jar', utils.R8_JAR, 'printseeds',
+    utils.RT_JAR, utils.R8_JAR, utils.R8LIB_KEEP_RULES,
+  ]
+  write_sorted_lines(printseeds_args, printseeds_path)
+
+  printuses_path = os.path.join(output_dir, 'sample-uses.txt')
+  printuses_args = [
+    'java', '-jar', utils.R8_JAR, 'printuses',
+    utils.RT_JAR, utils.R8_JAR, API_SAMPLE_JAR,
+  ]
+  write_sorted_lines(printuses_args, printuses_path)
+
+  print_diff(printseeds_path, printuses_path)
+
+
+def write_sorted_lines(cmd_args, output_path):
+  utils.PrintCmd(cmd_args)
+  output_lines = subprocess.check_output(cmd_args).splitlines(True)
+  print("Write output to %s" % output_path)
+  output_lines.sort()
+  with open(output_path, 'w') as fp:
+    for line in output_lines:
+      fp.write(line)
+
+
+def print_diff(printseeds_path, printuses_path):
+  with open(printseeds_path) as fp:
+    seeds = set(fp.read().splitlines())
+  with open(printuses_path) as fp:
+    uses = set(fp.read().splitlines())
+  only_in_seeds = seeds - uses
+  only_in_uses = uses - seeds
+  if only_in_seeds:
+    print("%s lines with '-' are marked @Keep " % len(only_in_seeds) +
+          "but not used by sample.")
+  if only_in_uses:
+    print("%s lines with '+' are used by sample " % len(only_in_uses) +
+          "but are missing @Keep annotations.")
+  for line in sorted(only_in_seeds):
+    print('-' + line)
+  for line in sorted(only_in_uses):
+    print('+' + line)
+  if not only_in_seeds and not only_in_uses:
+    print('Sample uses the entire set of members marked @Keep. Well done!')
+
+
+if __name__ == '__main__':
+  main(**vars(parser.parse_args()))
diff --git a/tools/build_r8lib.py b/tools/build_r8lib.py
index abaa619..c855bc5 100755
--- a/tools/build_r8lib.py
+++ b/tools/build_r8lib.py
@@ -18,7 +18,6 @@
                                  formatter_class=argparse.RawTextHelpFormatter)
 
 SAMPLE_JAR = os.path.join(utils.REPO_ROOT, 'tests/d8_api_usage_sample.jar')
-KEEP_RULES = os.path.join(utils.REPO_ROOT, 'src/main/keep.txt')
 R8LIB_JAR = os.path.join(utils.LIBS, 'r8lib.jar')
 R8LIB_MAP_FILE = os.path.join(utils.LIBS, 'r8lib-map.txt')
 
@@ -34,7 +33,7 @@
        '--lib', utils.RT_JAR,
        utils.R8_JAR,
        '--output', R8LIB_JAR,
-       '--pg-conf', KEEP_RULES,
+       '--pg-conf', utils.R8LIB_KEEP_RULES,
        '--pg-map-output', R8LIB_MAP_FILE))
 
 
diff --git a/tools/printseeds.py b/tools/printseeds.py
new file mode 100755
index 0000000..4f389d5
--- /dev/null
+++ b/tools/printseeds.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('printseeds', sys.argv[1:]))
diff --git a/tools/printuses.py b/tools/printuses.py
new file mode 100755
index 0000000..17d3df1
--- /dev/null
+++ b/tools/printuses.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('printuses', sys.argv[1:]))
diff --git a/tools/utils.py b/tools/utils.py
index 859da97..851f3bd 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -37,6 +37,7 @@
 MAVEN_ZIP = os.path.join(LIBS, 'r8.zip')
 GENERATED_LICENSE = os.path.join(GENERATED_LICENSE_DIR, 'LICENSE')
 RT_JAR = os.path.join(REPO_ROOT, 'third_party/openjdk/openjdk-rt-1.8/rt.jar')
+R8LIB_KEEP_RULES = os.path.join(REPO_ROOT, 'src/main/keep.txt')
 
 def PrintCmd(s):
   if type(s) is list: