Merge "No logging of output when running with --golem on run_on_as_app"
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 7b2c688..ab06751 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -26,6 +26,7 @@
 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.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
@@ -49,7 +50,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -227,77 +228,58 @@
       SortAnnotations sortAnnotations = new SortAnnotations();
       application.classes().forEach((clazz) -> clazz.addDependencies(sortAnnotations));
 
-      // Collect the indexed items sets for all files and perform JumboString processing.
-      // This is required to ensure that shared code blocks have a single and consistent code
-      // item that is valid for all dex files.
-      // Use a linked hash map as the order matters when addDexProgramData is called below.
-      Map<VirtualFile, Future<ObjectToOffsetMapping>> offsetMappingFutures = new LinkedHashMap<>();
-      for (VirtualFile newFile : distribute(executorService)) {
-        if (!newFile.isEmpty()) {
-          offsetMappingFutures
-              .put(newFile, executorService.submit(() -> {
-                ObjectToOffsetMapping mapping = newFile.computeMapping(application);
-                rewriteCodeWithJumboStrings(mapping, newFile.classes(), application);
-                return mapping;
-              }));
-        }
-      }
-
-      // Wait for all spawned futures to terminate to ensure jumbo string writing is complete.
-      ThreadUtils.awaitFutures(offsetMappingFutures.values());
-
       // Generate the dex file contents.
       List<Future<Boolean>> dexDataFutures = new ArrayList<>();
-      try {
-        for (VirtualFile virtualFile : offsetMappingFutures.keySet()) {
-          assert !virtualFile.isEmpty();
-          final ObjectToOffsetMapping mapping = offsetMappingFutures.get(virtualFile).get();
-          dexDataFutures.add(
-              executorService.submit(
-                  () -> {
-                    ProgramConsumer consumer;
-                    ByteBufferProvider byteBufferProvider;
-                    if (programConsumer != null) {
-                      consumer = programConsumer;
-                      byteBufferProvider = programConsumer;
-                    } else if (virtualFile.getPrimaryClassDescriptor() != null) {
-                      consumer = options.getDexFilePerClassFileConsumer();
-                      byteBufferProvider = options.getDexFilePerClassFileConsumer();
-                    } else {
-                      consumer = options.getDexIndexedConsumer();
-                      byteBufferProvider = options.getDexIndexedConsumer();
-                    }
-                    ByteBufferResult result = writeDexFile(mapping, byteBufferProvider);
-                    ByteDataView data =
-                        new ByteDataView(
-                            result.buffer.array(), result.buffer.arrayOffset(), result.length);
-                    if (consumer instanceof DexFilePerClassFileConsumer) {
-                      ((DexFilePerClassFileConsumer) consumer)
-                          .accept(
-                              virtualFile.getPrimaryClassDescriptor(),
-                              data,
-                              virtualFile.getClassDescriptors(),
-                              options.reporter);
-                    } else {
-                      ((DexIndexedConsumer) consumer)
-                          .accept(
-                              virtualFile.getId(),
-                              data,
-                              virtualFile.getClassDescriptors(),
-                              options.reporter);
-                    }
-                    // Release use of the backing buffer now that accept has returned.
-                    data.invalidate();
-                    byteBufferProvider.releaseByteBuffer(result.buffer.asByteBuffer());
-                    return true;
-                  }));
+      Iterable<VirtualFile> virtualFiles = distribute(executorService);
+      for (VirtualFile virtualFile : virtualFiles) {
+        if (virtualFile.isEmpty()) {
+          continue;
         }
-      } catch (InterruptedException e) {
-        throw new RuntimeException("Interrupted while waiting for future.", e);
+        dexDataFutures.add(
+            executorService.submit(
+                () -> {
+                  ProgramConsumer consumer;
+                  ByteBufferProvider byteBufferProvider;
+                  if (programConsumer != null) {
+                    consumer = programConsumer;
+                    byteBufferProvider = programConsumer;
+                  } else if (virtualFile.getPrimaryClassDescriptor() != null) {
+                    consumer = options.getDexFilePerClassFileConsumer();
+                    byteBufferProvider = options.getDexFilePerClassFileConsumer();
+                  } else {
+                    consumer = options.getDexIndexedConsumer();
+                    byteBufferProvider = options.getDexIndexedConsumer();
+                  }
+                  ObjectToOffsetMapping objectMapping = virtualFile.computeMapping(application);
+                  MethodToCodeObjectMapping codeMapping =
+                      rewriteCodeWithJumboStrings(
+                          objectMapping, virtualFile.classes(), application);
+                  ByteBufferResult result =
+                      writeDexFile(objectMapping, codeMapping, byteBufferProvider);
+                  ByteDataView data =
+                      new ByteDataView(
+                          result.buffer.array(), result.buffer.arrayOffset(), result.length);
+                  if (consumer instanceof DexFilePerClassFileConsumer) {
+                    ((DexFilePerClassFileConsumer) consumer)
+                        .accept(
+                            virtualFile.getPrimaryClassDescriptor(),
+                            data,
+                            virtualFile.getClassDescriptors(),
+                            options.reporter);
+                  } else {
+                    ((DexIndexedConsumer) consumer)
+                        .accept(
+                            virtualFile.getId(),
+                            data,
+                            virtualFile.getClassDescriptors(),
+                            options.reporter);
+                  }
+                  // Release use of the backing buffer now that accept has returned.
+                  data.invalidate();
+                  byteBufferProvider.releaseByteBuffer(result.buffer.asByteBuffer());
+                  return true;
+                }));
       }
-
-      // Clear out the map, as it is no longer needed.
-      offsetMappingFutures.clear();
       // Wait for all files to be processed before moving on.
       ThreadUtils.awaitFutures(dexDataFutures);
       // Fail if there are pending errors, e.g., the program consumers may have reported errors.
@@ -482,36 +464,58 @@
   }
 
   /**
-   * Rewrites the code for all methods in the given file so that they use JumboString for at
-   * least the strings that require it in mapping.
-   * <p>
-   * If run multiple times on a class, the lowest index that is required to be a JumboString will
+   * Rewrites the code for all methods in the given file so that they use JumboString for at least
+   * the strings that require it in mapping.
+   *
+   * <p>If run multiple times on a class, the lowest index that is required to be a JumboString will
    * be used.
    */
-  private void rewriteCodeWithJumboStrings(ObjectToOffsetMapping mapping,
-      Collection<DexProgramClass> classes, DexApplication application) {
+  private MethodToCodeObjectMapping rewriteCodeWithJumboStrings(
+      ObjectToOffsetMapping mapping,
+      Collection<DexProgramClass> classes,
+      DexApplication application) {
     // Do not bail out early if forcing jumbo string processing.
     if (!options.testing.forceJumboStringProcessing) {
       // If there are no strings with jumbo indices at all this is a no-op.
       if (!mapping.hasJumboStrings()) {
-        return;
+        return MethodToCodeObjectMapping.fromMethodBacking();
       }
       // If the globally highest sorting string is not a jumbo string this is also a no-op.
       if (application.highestSortingString != null &&
           application.highestSortingString.slowCompareTo(mapping.getFirstJumboString()) < 0) {
-        return;
+        return MethodToCodeObjectMapping.fromMethodBacking();
       }
     }
-    // At least one method needs a jumbo string.
+    // At least one method needs a jumbo string in which case we construct a thread local mapping
+    // for all code objects and write the processed results into that map.
+    Map<DexEncodedMethod, DexCode> codeMapping = new IdentityHashMap<>();
     for (DexProgramClass clazz : classes) {
-      clazz.forEachMethod(method -> method.rewriteCodeWithJumboStrings(
-          mapping, application, options.testing.forceJumboStringProcessing));
+      boolean isSharedSynthetic = clazz.getSynthesizedFrom().size() > 1;
+      clazz.forEachMethod(
+          method -> {
+            DexCode code =
+                method.rewriteCodeWithJumboStrings(
+                    mapping,
+                    application.dexItemFactory,
+                    options.testing.forceJumboStringProcessing);
+            codeMapping.put(method, code);
+            if (!isSharedSynthetic) {
+              // If the class is not a shared class the mapping now has ownership of the methods
+              // code object. This ensures freeing of code resources once the map entry is cleared
+              // and also ensures that we don't end up using the incorrect code pointer again later!
+              method.removeCode();
+            }
+          });
     }
+    return MethodToCodeObjectMapping.fromMapBacking(codeMapping);
   }
 
   private ByteBufferResult writeDexFile(
-      ObjectToOffsetMapping mapping, ByteBufferProvider provider) {
-    FileWriter fileWriter = new FileWriter(provider, mapping, application, options, namingLens);
+      ObjectToOffsetMapping objectMapping,
+      MethodToCodeObjectMapping codeMapping,
+      ByteBufferProvider provider) {
+    FileWriter fileWriter =
+        new FileWriter(provider, objectMapping, codeMapping, application, options, namingLens);
     // Collect the non-fixed sections.
     fileWriter.collect();
     // Generate and write the bytes.
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 1cee816..fa44619 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -58,7 +58,7 @@
 import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.security.MessageDigest;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -86,6 +86,7 @@
   }
 
   private final ObjectToOffsetMapping mapping;
+  private final MethodToCodeObjectMapping codeMapping;
   private final DexApplication application;
   private final InternalOptions options;
   private final NamingLens namingLens;
@@ -95,19 +96,21 @@
   public FileWriter(
       ByteBufferProvider provider,
       ObjectToOffsetMapping mapping,
+      MethodToCodeObjectMapping codeMapping,
       DexApplication application,
       InternalOptions options,
       NamingLens namingLens) {
     this.mapping = mapping;
+    this.codeMapping = codeMapping;
     this.application = application;
     this.options = options;
     this.namingLens = namingLens;
     this.dest = new DexOutputBuffer(provider);
-    this.mixedSectionOffsets = new MixedSectionOffsets(options);
+    this.mixedSectionOffsets = new MixedSectionOffsets(options, codeMapping);
   }
 
-  public static void writeEncodedAnnotation(DexEncodedAnnotation annotation, DexOutputBuffer dest,
-      ObjectToOffsetMapping mapping) {
+  public static void writeEncodedAnnotation(
+      DexEncodedAnnotation annotation, DexOutputBuffer dest, ObjectToOffsetMapping mapping) {
     if (Log.ENABLED) {
       Log.verbose(FileWriter.class, "Writing encoded annotation @ %08x", dest.position());
     }
@@ -154,8 +157,11 @@
     Layout layout = Layout.from(mapping);
     layout.setCodesOffset(layout.dataSectionOffset);
 
+    // Check code objects in the code-mapping are consistent with the collected code objects.
+    assert codeMapping.verifyCodeObjects(mixedSectionOffsets.getCodes());
+
     // Sort the codes first, as their order might impact size due to alignment constraints.
-    List<DexCode> codes = sortDexCodesByClassName(mixedSectionOffsets.getCodes(), application);
+    List<DexCode> codes = sortDexCodesByClassName();
 
     // Output the debug_info_items first, as they have no dependencies.
     dest.moveTo(layout.getCodesOffset() + sizeOfCodeItems(codes));
@@ -276,35 +282,40 @@
     }
   }
 
-  private List<DexCode> sortDexCodesByClassName(Collection<DexCode> codes,
-      DexApplication application) {
+  private List<DexCode> sortDexCodesByClassName() {
     Map<DexCode, String> codeToSignatureMap = new IdentityHashMap<>();
+    List<DexCode> codesSorted = new ArrayList<>();
     for (DexProgramClass clazz : mapping.getClasses()) {
-      clazz.forEachMethod(method ->
-          addSignaturesFromMethod(method, codeToSignatureMap, application.getProguardMap()));
+      clazz.forEachMethod(
+          method -> {
+            DexCode code = codeMapping.getCode(method);
+            assert code != null || method.shouldNotHaveCode();
+            if (code != null) {
+              codesSorted.add(code);
+              addSignaturesFromMethod(
+                  method, code, codeToSignatureMap, application.getProguardMap());
+            }
+          });
     }
-    DexCode[] codesArray = codes.toArray(new DexCode[codes.size()]);
-    Arrays.sort(codesArray, Comparator.comparing(codeToSignatureMap::get));
-    return Arrays.asList(codesArray);
+    codesSorted.sort(Comparator.comparing(codeToSignatureMap::get));
+    return codesSorted;
   }
 
-  private static void addSignaturesFromMethod(DexEncodedMethod method,
+  private static void addSignaturesFromMethod(
+      DexEncodedMethod method,
+      DexCode code,
       Map<DexCode, String> codeToSignatureMap,
       ClassNameMapper proguardMap) {
-    if (!method.hasCode()) {
-      assert method.shouldNotHaveCode();
+    Signature signature;
+    String originalClassName;
+    if (proguardMap != null) {
+      signature = proguardMap.originalSignatureOf(method.method);
+      originalClassName = proguardMap.originalNameOf(method.method.holder);
     } else {
-      Signature signature;
-      String originalClassName;
-      if (proguardMap != null) {
-        signature = proguardMap.originalSignatureOf(method.method);
-        originalClassName = proguardMap.originalNameOf(method.method.holder);
-      } else {
-        signature = MethodSignature.fromDexMethod(method.method);
-        originalClassName = method.method.holder.toSourceString();
-      }
-      codeToSignatureMap.put(method.getCode().asDexCode(), originalClassName + signature);
+      signature = MethodSignature.fromDexMethod(method.method);
+      originalClassName = method.method.holder.toSourceString();
     }
+    codeToSignatureMap.put(code, originalClassName + signature);
   }
 
   private <T extends IndexedDexItem> void writeFixedSectionItems(
@@ -573,7 +584,7 @@
     }
   }
 
-  private void writeEncodedMethods(List<DexEncodedMethod> methods, boolean clearBodies) {
+  private void writeEncodedMethods(List<DexEncodedMethod> methods, boolean isSharedSynthetic) {
     assert PresortedComparable.isSorted(methods);
     int currentOffset = 0;
     for (DexEncodedMethod method : methods) {
@@ -582,16 +593,15 @@
       dest.putUleb128(nextOffset - currentOffset);
       currentOffset = nextOffset;
       dest.putUleb128(method.accessFlags.getAsDexAccessFlags());
-      if (!method.hasCode()) {
+      DexCode code = codeMapping.getCode(method);
+      if (code == null) {
         assert method.shouldNotHaveCode();
         dest.putUleb128(0);
       } else {
-        dest.putUleb128(mixedSectionOffsets.getOffsetFor(method.getCode().asDexCode()));
+        dest.putUleb128(mixedSectionOffsets.getOffsetFor(code));
         // Writing the methods starts to take up memory so we are going to flush the
         // code objects since they are no longer necessary after this.
-        if (clearBodies) {
-          method.removeCode();
-        }
+        codeMapping.clearCode(method, isSharedSynthetic);
       }
     }
   }
@@ -605,10 +615,9 @@
     dest.putUleb128(clazz.virtualMethods().size());
     writeEncodedFields(clazz.staticFields());
     writeEncodedFields(clazz.instanceFields());
-
     boolean isSharedSynthetic = clazz.getSynthesizedFrom().size() > 1;
-    writeEncodedMethods(clazz.directMethods(), !isSharedSynthetic);
-    writeEncodedMethods(clazz.virtualMethods(), !isSharedSynthetic);
+    writeEncodedMethods(clazz.directMethods(), isSharedSynthetic);
+    writeEncodedMethods(clazz.virtualMethods(), isSharedSynthetic);
   }
 
   private void addStaticFieldValues(DexProgramClass clazz) {
@@ -1001,6 +1010,8 @@
     private static final int NOT_SET = -1;
     private static final int NOT_KNOWN = -2;
 
+    private final MethodToCodeObjectMapping codeMapping;
+
     private final Reference2IntMap<DexCode> codes = createReference2IntMap();
     private final Object2IntMap<DexDebugInfo> debugInfos = createObject2IntMap();
     private final Object2IntMap<DexTypeList> typeLists = createObject2IntMap();
@@ -1030,8 +1041,9 @@
       return result;
     }
 
-    private MixedSectionOffsets(InternalOptions options) {
+    private MixedSectionOffsets(InternalOptions options, MethodToCodeObjectMapping codeMapping) {
       this.minApiLevel = options.minApiLevel;
+      this.codeMapping = codeMapping;
     }
 
     private <T> boolean add(Object2IntMap<T> map, T item) {
@@ -1071,6 +1083,11 @@
     }
 
     @Override
+    public void visit(DexEncodedMethod method) {
+      method.collectMixedSectionItemsWithCodeMapping(this, codeMapping);
+    }
+
+    @Override
     public boolean add(DexCode code) {
       return add(codes, code);
     }
diff --git a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
index 1c18107..b2a3e09 100644
--- a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
+++ b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
@@ -107,7 +107,7 @@
     this.factory = factory;
   }
 
-  public void rewrite() {
+  public DexCode rewrite() {
     // Build maps from everything in the code that uses offsets or direct addresses to reference
     // instructions to the actual instruction referenced.
     recordTargets();
@@ -133,7 +133,7 @@
     // As we have rewritten the code, we now know that its highest string index that is not
     // a jumbo-string is firstJumboString (actually the previous string, but we do not have that).
     newCode.highestSortingString = firstJumboString;
-    method.setCode(newCode);
+    return newCode;
   }
 
   private void rewriteInstructionOffsets(List<Instruction> instructions) {
diff --git a/src/main/java/com/android/tools/r8/dex/MethodToCodeObjectMapping.java b/src/main/java/com/android/tools/r8/dex/MethodToCodeObjectMapping.java
new file mode 100644
index 0000000..feeb64b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/dex/MethodToCodeObjectMapping.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2019, 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.dex;
+
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import java.util.Collection;
+import java.util.Map;
+
+public abstract class MethodToCodeObjectMapping {
+
+  public abstract DexCode getCode(DexEncodedMethod method);
+
+  public abstract void clearCode(DexEncodedMethod method, boolean isSharedSynthetic);
+
+  public abstract boolean verifyCodeObjects(Collection<DexCode> codes);
+
+  public static MethodToCodeObjectMapping fromMethodBacking() {
+    return MethodBacking.INSTANCE;
+  }
+
+  public static MethodToCodeObjectMapping fromMapBacking(Map<DexEncodedMethod, DexCode> map) {
+    return new MapBacking(map);
+  }
+
+  private static class MethodBacking extends MethodToCodeObjectMapping {
+
+    private static final MethodBacking INSTANCE = new MethodBacking();
+
+    @Override
+    public DexCode getCode(DexEncodedMethod method) {
+      Code code = method.getCode();
+      assert code == null || code.isDexCode();
+      return code == null ? null : code.asDexCode();
+    }
+
+    @Override
+    public void clearCode(DexEncodedMethod method, boolean isSharedSynthetic) {
+      // When using methods directly any shared class needs to maintain its methods as read-only.
+      if (!isSharedSynthetic) {
+        method.removeCode();
+      }
+    }
+
+    @Override
+    public boolean verifyCodeObjects(Collection<DexCode> codes) {
+      return true;
+    }
+  }
+
+  private static class MapBacking extends MethodToCodeObjectMapping {
+
+    private final Map<DexEncodedMethod, DexCode> codes;
+
+    public MapBacking(Map<DexEncodedMethod, DexCode> codes) {
+      this.codes = codes;
+    }
+
+    @Override
+    public DexCode getCode(DexEncodedMethod method) {
+      return codes.get(method);
+    }
+
+    @Override
+    public void clearCode(DexEncodedMethod method, boolean isSharedSynthetic) {
+      // We can safely clear the thread local pointer to even shared methods.
+      codes.put(method, null);
+    }
+
+    @Override
+    public boolean verifyCodeObjects(Collection<DexCode> codes) {
+      assert this.codes.values().containsAll(codes);
+      return true;
+    }
+  }
+}
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 dee12fc..2cedf5c 100644
--- a/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java
+++ b/src/main/java/com/android/tools/r8/dex/MixedSectionCollection.java
@@ -9,6 +9,7 @@
 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.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItem;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexTypeList;
@@ -53,6 +54,16 @@
   public abstract boolean add(DexAnnotationSet dexAnnotationSet);
 
   /**
+   * Recurse on the given encoded method to add items to the collection.
+   *
+   * <p>Allows overriding the behavior when dex-file writing.
+   */
+  public void visit(DexEncodedMethod dexEncodedMethod) {
+    dexEncodedMethod.collectMixedSectionItemsWithCodeMapping(
+        this, MethodToCodeObjectMapping.fromMethodBacking());
+  }
+
+  /**
    * Adds the given code item to the collection.
    *
    * Does not add any dependencies.
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 9fa16f1..d4699ca 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.JumboStringRewriter;
+import com.android.tools.r8.dex.MethodToCodeObjectMapping;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
@@ -366,6 +367,12 @@
 
   @Override
   void collectMixedSectionItems(MixedSectionCollection mixedItems) {
+    mixedItems.visit(this);
+  }
+
+  public void collectMixedSectionItemsWithCodeMapping(
+      MixedSectionCollection mixedItems, MethodToCodeObjectMapping mapping) {
+    DexCode code = mapping.getCode(this);
     if (code != null) {
       code.collectMixedSectionItems(mixedItems);
     }
@@ -678,19 +685,13 @@
     return method;
   }
 
-  /**
-   * Rewrites the code in this method to have JumboString bytecode if required by mapping.
-   * <p>
-   * Synchronized such that it can be called concurrently for different mappings. As a side-effect,
-   * this will also update the highestSortingString to the index of the strings up to which the code
-   * was rewritten to avoid rewriting again unless needed.
-   */
-  public synchronized void rewriteCodeWithJumboStrings(
-      ObjectToOffsetMapping mapping, DexApplication application, boolean force) {
+  /** Rewrites the code in this method to have JumboString bytecode if required by mapping. */
+  public DexCode rewriteCodeWithJumboStrings(
+      ObjectToOffsetMapping mapping, DexItemFactory factory, boolean force) {
     checkIfObsolete();
     assert code == null || code.isDexCode();
     if (code == null) {
-      return;
+      return null;
     }
     DexCode code = this.code.asDexCode();
     DexString firstJumboString = null;
@@ -706,10 +707,10 @@
       }
     }
     if (firstJumboString != null) {
-      JumboStringRewriter rewriter =
-          new JumboStringRewriter(this, firstJumboString, application.dexItemFactory);
-      rewriter.rewrite();
+      JumboStringRewriter rewriter = new JumboStringRewriter(this, firstJumboString, factory);
+      return rewriter.rewrite();
     }
+    return code;
   }
 
   public String codeToString() {
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 9f7d021..07b32ec 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -177,7 +177,7 @@
     reporter.failIfPendingErrors();
   }
 
-  private static enum IdentifierType {
+  private enum IdentifierType {
     CLASS_NAME,
     ANY
   }
@@ -288,11 +288,9 @@
         }
         skipWhitespace();
         char quote = acceptQuoteIfPresent();
+        configurationBuilder.setPackagePrefix(parsePackageNameOrEmptyString());
         if (isQuote(quote)) {
-          configurationBuilder.setPackagePrefix(parsePackageNameOrEmptyString());
           expectClosingQuote(quote);
-        } else {
-          configurationBuilder.setPackagePrefix("");
         }
       } else if (acceptString("flattenpackagehierarchy")) {
         if (configurationBuilder.getPackageObfuscationMode() == PackageObfuscationMode.REPACKAGE) {
@@ -304,11 +302,9 @@
         } else {
           skipWhitespace();
           char quote = acceptQuoteIfPresent();
+          configurationBuilder.setFlattenPackagePrefix(parsePackageNameOrEmptyString());
           if (isQuote(quote)) {
-            configurationBuilder.setFlattenPackagePrefix(parsePackageNameOrEmptyString());
             expectClosingQuote(quote);
-          } else {
-            configurationBuilder.setFlattenPackagePrefix("");
           }
         }
       } else if (acceptString("overloadaggressively")) {
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 208fb9c..d408623 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -906,7 +906,8 @@
     if (context instanceof ProguardKeepRule) {
       if (item.isDexEncodedMethod()) {
         DexEncodedMethod encodedMethod = item.asDexEncodedMethod();
-        if (encodedMethod.method.isLambdaDeserializeMethod(appView.dexItemFactory())) {
+        if (options.isGeneratingDex()
+            && encodedMethod.method.isLambdaDeserializeMethod(appView.dexItemFactory())) {
           // Don't keep lambda deserialization methods.
           return;
         }
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 5fccb4a..6eea205 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -9,6 +9,7 @@
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.concurrent.ExecutionException;
 
 public abstract class TestBuilder<RR extends TestRunResult, T extends TestBuilder<RR, T>> {
 
@@ -34,18 +35,20 @@
   }
 
   @Deprecated
-  public abstract RR run(String mainClass) throws IOException, CompilationFailedException;
+  public abstract RR run(String mainClass)
+      throws CompilationFailedException, ExecutionException, IOException;
 
   public abstract RR run(TestRuntime runtime, String mainClass)
-      throws IOException, CompilationFailedException;
+      throws CompilationFailedException, ExecutionException, IOException;
 
   @Deprecated
-  public RR run(Class mainClass) throws IOException, CompilationFailedException {
+  public RR run(Class<?> mainClass)
+      throws CompilationFailedException, ExecutionException, IOException {
     return run(mainClass.getTypeName());
   }
 
-  public RR run(TestRuntime runtime, Class mainClass)
-      throws IOException, CompilationFailedException {
+  public RR run(TestRuntime runtime, Class<?> mainClass)
+      throws CompilationFailedException, ExecutionException, IOException {
     return run(runtime, mainClass.getTypeName());
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 7fcc59a..1c4d7e1 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -4,6 +4,8 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.TestBase.Backend.DEX;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
 import com.android.tools.r8.TestBase.Backend;
@@ -15,6 +17,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -48,33 +51,38 @@
   protected abstract RR createRunResult(ProcessResult result);
 
   @Deprecated
-  public RR run(Class<?> mainClass) throws IOException {
+  public RR run(Class<?> mainClass) throws ExecutionException, IOException {
     return run(mainClass.getTypeName());
   }
 
   @Deprecated
-  public RR run(String mainClass) throws IOException {
+  public RR run(String mainClass) throws ExecutionException, IOException {
+    ClassSubject mainClassSubject = inspector().clazz(mainClass);
+    assertThat(mainClassSubject, isPresent());
     switch (getBackend()) {
       case DEX:
-        return runArt(null, additionalRunClassPath, mainClass);
+        return runArt(null, additionalRunClassPath, mainClassSubject.getFinalName());
       case CF:
-        return runJava(null, additionalRunClassPath, mainClass);
+        return runJava(null, additionalRunClassPath, mainClassSubject.getFinalName());
       default:
         throw new Unreachable();
     }
   }
 
-  public RR run(TestRuntime runtime, Class<?> mainClass) throws IOException {
+  public RR run(TestRuntime runtime, Class<?> mainClass) throws ExecutionException, IOException {
     return run(runtime, mainClass.getTypeName());
   }
 
-  public RR run(TestRuntime runtime, String mainClass) throws IOException {
+  public RR run(TestRuntime runtime, String mainClass) throws ExecutionException, IOException {
     assert getBackend() == runtime.getBackend();
+    ClassSubject mainClassSubject = inspector().clazz(mainClass);
+    assertThat(mainClassSubject, isPresent());
     if (runtime.isDex()) {
-      return runArt(runtime.asDex().getVm(), additionalRunClassPath, mainClass);
+      return runArt(
+          runtime.asDex().getVm(), additionalRunClassPath, mainClassSubject.getFinalName());
     }
     assert runtime.isCf();
-    return runJava(runtime, additionalRunClassPath, mainClass);
+    return runJava(runtime, additionalRunClassPath, mainClassSubject.getFinalName());
   }
 
   public CR addRunClasspathFiles(Path... classpath) {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 8dbc309..c648668 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -14,6 +14,7 @@
 import java.io.PrintStream;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
@@ -85,13 +86,14 @@
   }
 
   @Override
-  public RR run(String mainClass) throws IOException, CompilationFailedException {
+  public RR run(String mainClass)
+      throws CompilationFailedException, ExecutionException, IOException {
     return compile().run(mainClass);
   }
 
   @Override
   public RR run(TestRuntime runtime, String mainClass)
-      throws IOException, CompilationFailedException {
+      throws CompilationFailedException, ExecutionException, IOException {
     return compile().run(runtime, mainClass);
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index e2f4239..f22b23e 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -83,6 +83,12 @@
     return self();
   }
 
+  public RR assertSuccessWithOutputThatMatches(Matcher<String> matcher) {
+    assertSuccess();
+    assertThat(errorMessage("Run stdout incorrect.", matcher.toString()), result.stdout, matcher);
+    return self();
+  }
+
   public CodeInspector inspector() throws IOException, ExecutionException {
     // Inspection post run implies success. If inspection of an invalid program is needed it should
     // be done on the compilation result or on the input.
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 511f328..5e3e100 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -18,7 +18,7 @@
         B extends BaseCompilerCommand.Builder<C, B>,
         CR extends TestCompileResult<CR, RR>,
         RR extends TestRunResult,
-        T extends TestCompilerBuilder<C, B, CR, RR, T>>
+        T extends TestShrinkerBuilder<C, B, CR, RR, T>>
     extends TestCompilerBuilder<C, B, CR, RR, T> {
 
   protected boolean enableMinification = true;
diff --git a/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTest.java b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTest.java
new file mode 100644
index 0000000..4dc81f4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTest.java
@@ -0,0 +1,60 @@
+// Copyright (c) 2019, 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.cf;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+class KeepDeserializeLambdaMethodTest {
+  static final String LAMBDA_MESSAGE = "[I'm the lambda.]";
+
+  static void invokeLambda(Object o)
+      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+    Method runMethod = o.getClass().getMethod("run");
+    runMethod.invoke(o);
+  }
+}
+
+class KeepDeserializeLambdaMethodTestDex extends KeepDeserializeLambdaMethodTest {
+  public static void main(String[] args) throws Exception {
+    Serializable myLambda =
+        (Runnable & Serializable)
+            () -> System.out.println(KeepDeserializeLambdaMethodTest.LAMBDA_MESSAGE);
+    invokeLambda(myLambda);
+  }
+}
+
+class KeepDeserializeLambdaMethodTestCf extends KeepDeserializeLambdaMethodTest {
+
+  public static void main(String[] args) throws Exception {
+    Serializable myLambda = (Runnable & Serializable) () -> System.out.println(LAMBDA_MESSAGE);
+
+    byte[] bytes;
+    {
+      ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+      ObjectOutputStream out = new ObjectOutputStream(byteStream);
+      out.writeObject(myLambda);
+      out.close();
+      bytes = byteStream.toByteArray();
+    }
+    Object o;
+    {
+      ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
+      ObjectInputStream in = new ObjectInputStream(byteStream);
+      o = in.readObject();
+      in.close();
+    }
+    String name = o.getClass().getName();
+    if (!name.contains("KeepDeserializeLambdaMethodTestCf")) {
+      throw new RuntimeException("Unexpected class name " + name);
+    }
+    invokeLambda(o);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java
new file mode 100644
index 0000000..c2dc7b7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/KeepDeserializeLambdaMethodTestRunner.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2019, 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.cf;
+
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+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 KeepDeserializeLambdaMethodTestRunner extends TestBase {
+
+  private static final Class TEST_CLASS_CF =
+      com.android.tools.r8.cf.KeepDeserializeLambdaMethodTestCf.class;
+  private static final Class TEST_CLASS_DEX =
+      com.android.tools.r8.cf.KeepDeserializeLambdaMethodTestDex.class;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withCfRuntimes().withDexRuntime(Version.last()).build();
+  }
+
+  private final TestParameters parameters;
+
+  public KeepDeserializeLambdaMethodTestRunner(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNoTreeShakingCf() throws Exception {
+    test(false);
+  }
+
+  @Test
+  public void testKeepRuleCf() throws Exception {
+    test(true);
+  }
+
+  private void test(boolean keepRule)
+      throws IOException, CompilationFailedException, ExecutionException {
+    Class testClass = parameters.isCfRuntime() ? TEST_CLASS_CF : TEST_CLASS_DEX;
+    R8TestBuilder builder =
+        testForR8Compat(parameters.getBackend())
+            .addProgramClasses(
+                com.android.tools.r8.cf.KeepDeserializeLambdaMethodTest.class, testClass)
+            .apply(parameters::setMinApiForRuntime)
+            .addKeepMainRule(testClass)
+            .noMinification();
+    if (keepRule) {
+      builder.addKeepRules(
+          "-keepclassmembers class * {",
+          "private static synthetic java.lang.Object "
+              + "$deserializeLambda$(java.lang.invoke.SerializedLambda);",
+          "}");
+    } else {
+      builder.noTreeShaking();
+    }
+    R8TestRunResult result =
+        builder
+            .run(parameters.getRuntime(), testClass)
+            .assertSuccessWithOutputThatMatches(
+                containsString(KeepDeserializeLambdaMethodTest.LAMBDA_MESSAGE))
+            .inspect(
+                inspector -> {
+                  MethodSubject method =
+                      inspector.clazz(testClass).uniqueMethodWithName("$deserializeLambda$");
+                  assertEquals(parameters.isCfRuntime(), method.isPresent());
+                });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java b/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
index e11b957..95e1e76 100644
--- a/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
+++ b/src/test/java/com/android/tools/r8/dex/JumboStringProcessing.java
@@ -159,7 +159,6 @@
         null);
     MethodAccessFlags flags = MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC, false);
     DexEncodedMethod method = new DexEncodedMethod(null, flags, null, null, code);
-    new JumboStringRewriter(method, string, factory).rewrite();
-    return method.getCode().asDexCode();
+    return new JumboStringRewriter(method, string, factory).rewrite();
   }
 }
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 df2d769..298ffe1 100644
--- a/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
+++ b/src/test/java/com/android/tools/r8/dex/SharedClassWritingTest.java
@@ -48,7 +48,6 @@
 import java.util.concurrent.ExecutorService;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class SharedClassWritingTest {
@@ -111,7 +110,6 @@
         synthesizedFrom);
   }
 
-  @Ignore("b/128281550")
   @Test
   public void manyFilesWithSharedSynthesizedClass() throws ExecutionException, IOException {
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ExtraMethodNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ExtraMethodNullTest.java
index 42b0ce4..3ece2da 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ExtraMethodNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ExtraMethodNullTest.java
@@ -6,16 +6,14 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
-import java.io.IOException;
 import org.junit.Test;
 
 public class ExtraMethodNullTest extends TestBase {
 
   @Test
-  public void test() throws IOException, CompilationFailedException {
+  public void test() throws Exception {
     testForR8(Backend.DEX)
         .addProgramClassesAndInnerClasses(One.class)
         .addKeepMainRule(One.class)
diff --git a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
index f599f4d..9ade2f9 100644
--- a/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/signature/GenericSignatureRenamingTest.java
@@ -4,11 +4,9 @@
 
 package com.android.tools.r8.naming.signature;
 
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
-import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
@@ -17,37 +15,37 @@
 public class GenericSignatureRenamingTest extends TestBase {
 
   @Test
-  public void testJVM() throws IOException, CompilationFailedException {
+  public void testJVM() throws Exception {
     testForJvm().addTestClasspath().run(Main.class).assertSuccess();
   }
 
   @Test
-  public void testR8Dex() throws IOException, CompilationFailedException {
+  public void testR8Dex() throws Exception {
     test(testForR8(Backend.DEX));
   }
 
   @Test
-  public void testR8CompatDex() throws IOException, CompilationFailedException {
+  public void testR8CompatDex() throws Exception {
     test(testForR8Compat(Backend.DEX));
   }
 
   @Test
-  public void testR8DexNoMinify() throws IOException, CompilationFailedException {
+  public void testR8DexNoMinify() throws Exception {
     test(testForR8(Backend.DEX).addKeepRules("-dontobfuscate"));
   }
 
   @Test
-  public void testR8Cf() throws IOException, CompilationFailedException {
+  public void testR8Cf() throws Exception {
     test(testForR8(Backend.CF));
   }
 
   @Test
-  public void testR8CfNoMinify() throws IOException, CompilationFailedException {
+  public void testR8CfNoMinify() throws Exception {
     test(testForR8(Backend.CF).addKeepRules("-dontobfuscate"));
   }
 
   @Test
-  public void testD8() throws IOException, CompilationFailedException {
+  public void testD8() throws Exception {
     testForD8()
         .addProgramClasses(Main.class)
         .addProgramClassesAndInnerClasses(A.class, B.class, CY.class, CYY.class)
@@ -58,7 +56,7 @@
         .assertSuccess();
   }
 
-  private void test(R8TestBuilder builder) throws IOException, CompilationFailedException {
+  private void test(R8TestBuilder builder) throws Exception {
     builder
         .addKeepRules("-dontoptimize")
         .addKeepRules("-keepattributes InnerClasses,EnclosingMethod,Signature")
diff --git a/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java b/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java
new file mode 100644
index 0000000..afaf999
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java
@@ -0,0 +1,135 @@
+// Copyright (c) 2019, 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.proguard.configuration;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+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 RepackagingCompatibilityTest extends TestBase {
+
+  private enum Quote {
+    SINGLE,
+    DOUBLE,
+    NONE
+  }
+
+  private static final String expectedOutput = StringUtils.lines("Hello world!");
+  private static final Class<?> mainClass = RepackagingCompatabilityTestClass.class;
+
+  private final String directive;
+  private final Quote quote;
+  private final boolean repackageToRoot;
+
+  @Parameters(name = "Directive: {0}, quote: {1}, repackage to root: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ImmutableList.of("-flattenpackagehierarchy", "-repackageclasses"),
+        Quote.values(),
+        BooleanUtils.values());
+  }
+
+  public RepackagingCompatibilityTest(String directive, Quote quote, boolean repackageToRoot) {
+    this.directive = directive;
+    this.quote = quote;
+    this.repackageToRoot = repackageToRoot;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    runTest(testForR8(Backend.DEX), "R8");
+  }
+
+  @Test
+  public void testProguard() throws Exception {
+    runTest(testForProguard(), "Proguard");
+  }
+
+  private void runTest(TestShrinkerBuilder<?, ?, ?, ?, ?> builder, String shrinker)
+      throws Exception {
+    assumeFalse(
+        String.format(
+            "Only repackage to root when there are no quotes"
+                + " (repackageToRoot: %s, quote: %s, shrinker: %s)",
+            repackageToRoot, quote, shrinker),
+        repackageToRoot && quote != Quote.NONE);
+
+    TestRunResult<?> result =
+        builder
+            .addProgramClasses(mainClass)
+            .addKeepRules(getKeepRules())
+            .run(mainClass)
+            .assertSuccessWithOutput(expectedOutput);
+
+    ClassSubject testClassSubject = result.inspector().clazz(mainClass);
+    assertThat(testClassSubject, isPresent());
+    if (repackageToRoot) {
+      if (directive.equals("-flattenpackagehierarchy")) {
+        assertThat(testClassSubject.getFinalName(), startsWith("a."));
+      } else if (directive.equals("-repackageclasses")) {
+        assertThat(testClassSubject.getFinalName(), not(containsString(".")));
+      } else {
+        fail();
+      }
+    } else {
+      assertThat(testClassSubject.getFinalName(), startsWith("greeter."));
+    }
+  }
+
+  private List<String> getKeepRules() {
+    return ImmutableList.of(
+        // Keep main(), but allow obfuscation
+        "-keep,allowobfuscation class " + mainClass.getTypeName() + " {",
+        "  public static void main(...);",
+        "}",
+        // Ensure main() is not renamed
+        "-keepclassmembernames class " + mainClass.getTypeName() + " {",
+        "  public static void main(...);",
+        "}",
+        getRepackagingRule());
+  }
+
+  private String getRepackagingRule() {
+    if (repackageToRoot) {
+      return directive;
+    }
+    switch (quote) {
+      case SINGLE:
+        return directive + " 'greeter'";
+      case DOUBLE:
+        return directive + " \"greeter\"";
+      case NONE:
+        return directive + " greeter";
+      default:
+        throw new Unreachable();
+    }
+  }
+}
+
+class RepackagingCompatabilityTestClass {
+
+  public static void main(String[] args) {
+    System.out.println("Hello world!");
+  }
+}