Merge "Reland "Implicitly add necessary rules for Java reflections in any R8 mode.""
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 16858af..bca3e00 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -490,6 +490,7 @@
         ClassNameMapper classNameMapper =
             LineNumberOptimizer.run(
                 application,
+                appView.getGraphLense(),
                 namingLens,
                 options.lineNumberOptimization == LineNumberOptimization.IDENTITY_MAPPING);
         timing.end();
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 98c523d..dcab5ae 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -117,6 +117,10 @@
     return clazz;
   }
 
+  public String qualifiedName() {
+    return clazz + "." + name;
+  }
+
   @Override
   public String toSmaliString() {
     return clazz.toSmaliString() + "->" + name + ":" + type.toSmaliString();
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index 84ceac3..d264b87 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -93,6 +93,10 @@
 
   public abstract DexMethod getOriginalMethodSignature(DexMethod method);
 
+  public abstract DexField getRenamedFieldSignature(DexField originalField);
+
+  public abstract DexMethod getRenamedMethodSignature(DexMethod originalMethod);
+
   public abstract DexType lookupType(DexType type);
 
   // This overload can be used when the graph lense is known to be context insensitive.
@@ -140,7 +144,7 @@
     return this instanceof IdentityGraphLense;
   }
 
-  public boolean assertNotModified(Iterable<DexItem> items) {
+  public <T extends DexItem> boolean assertNotModified(Iterable<T> items) {
     for (DexItem item : items) {
       if (item instanceof DexClass) {
         DexType type = ((DexClass) item).type;
@@ -172,6 +176,16 @@
     }
 
     @Override
+    public DexField getRenamedFieldSignature(DexField originalField) {
+      return originalField;
+    }
+
+    @Override
+    public DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
+      return originalMethod;
+    }
+
+    @Override
     public DexType lookupType(DexType type) {
       return type;
     }
@@ -254,6 +268,24 @@
     }
 
     @Override
+    public DexField getRenamedFieldSignature(DexField originalField) {
+      DexField renamedField =
+          originalFieldSignatures != null
+              ? originalFieldSignatures.inverse().getOrDefault(originalField, originalField)
+              : originalField;
+      return previousLense.getRenamedFieldSignature(renamedField);
+    }
+
+    @Override
+    public DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
+      DexMethod renamedMethod =
+          originalMethodSignatures != null
+              ? originalMethodSignatures.inverse().getOrDefault(originalMethod, originalMethod)
+              : originalMethod;
+      return previousLense.getRenamedMethodSignature(renamedMethod);
+    }
+
+    @Override
     public DexType lookupType(DexType type) {
       if (type.isArrayType()) {
         synchronized (this) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 631f234..998ebe9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -93,11 +93,11 @@
   private Reason computeInliningReason(DexEncodedMethod target) {
     if (target.getOptimizationInfo().forceInline()
         || (inliner.appInfo.hasLiveness()
-            && inliner.appInfo.withLiveness().forceInline.contains(target))) {
+            && inliner.appInfo.withLiveness().forceInline.contains(target.method))) {
       return Reason.FORCE;
     }
     if (inliner.appInfo.hasLiveness()
-        && inliner.appInfo.withLiveness().alwaysInline.contains(target)) {
+        && inliner.appInfo.withLiveness().alwaysInline.contains(target.method)) {
       return Reason.ALWAYS;
     }
     if (callSiteInformation.hasSingleCallSite(target)) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 12baa2c..1cbd2d9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -71,7 +71,7 @@
   }
 
   public boolean isBlackListed(DexEncodedMethod method) {
-    return blackList.contains(method.method) || appInfo.neverInline.contains(method);
+    return blackList.contains(method.method) || appInfo.neverInline.contains(method.method);
   }
 
   private ConstraintWithTarget instructionAllowedForInlining(
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
index c5d366a..e405bfb 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMapper.java
@@ -35,10 +35,10 @@
 public class ClassNameMapper implements ProguardMap {
 
   public static class Builder extends ProguardMap.Builder {
-    final ImmutableMap.Builder<String, ClassNamingForNameMapper.Builder> mapBuilder;
+    private final ImmutableMap.Builder<String, ClassNamingForNameMapper.Builder> mapBuilder;
 
     private Builder() {
-      mapBuilder = ImmutableMap.builder();
+      this.mapBuilder = ImmutableMap.builder();
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
index 34fdd9d..f45a3de 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -14,6 +14,7 @@
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -68,17 +69,17 @@
     /** The parameters are forwarded to MappedRange constructor, see explanation there. */
     @Override
     public void addMappedRange(
-        Range obfuscatedRange,
+        Range minifiedRange,
         MemberNaming.MethodSignature originalSignature,
         Object originalRange,
-        String obfuscatedName) {
+        String renamedName) {
       mappedRangesByName
-          .computeIfAbsent(obfuscatedName, k -> new ArrayList<>())
-          .add(new MappedRange(obfuscatedRange, originalSignature, originalRange, obfuscatedName));
+          .computeIfAbsent(renamedName, k -> new ArrayList<>())
+          .add(new MappedRange(minifiedRange, originalSignature, originalRange, renamedName));
     }
   }
 
-  /** List of MappedRanges that belong to the same obfuscated name. */
+  /** List of MappedRanges that belong to the same renamed name. */
   public static class MappedRangesOfName {
     private final List<MappedRange> mappedRanges;
 
@@ -93,14 +94,14 @@
     public MappedRange firstRangeForLine(int line) {
       MappedRange bestRange = null;
       for (MappedRange range : mappedRanges) {
-        if (range.obfuscatedRange == null) {
+        if (range.minifiedRange == null) {
           if (bestRange == null) {
             // This is an "a() -> b" mapping (no concrete line numbers), remember this if there'll
             // be no better one.
             bestRange = range;
           }
-        } else if (range.obfuscatedRange.contains(line)) {
-          // Concrete obfuscated range found ("x:y:a()[:u[:v]] -> b")
+        } else if (range.minifiedRange.contains(line)) {
+          // Concrete minified range found ("x:y:a()[:u[:v]] -> b")
           return range;
         }
       }
@@ -108,25 +109,25 @@
     }
 
     /**
-     * Search for a MappedRange where the obfuscated range contains the specified {@code line} and
-     * return that and the subsequent MappedRanges with the same obfuscated range. Return general
+     * Search for a MappedRange where the minified range contains the specified {@code line} and
+     * return that and the subsequent MappedRanges with the same minified range. Return general
      * MappedRange ("a() -> b") if no concrete mapping found or empty list if nothing found.
      */
     public List<MappedRange> allRangesForLine(int line) {
       MappedRange noLineRange = null;
       for (int i = 0; i < mappedRanges.size(); ++i) {
         MappedRange rangeI = mappedRanges.get(i);
-        if (rangeI.obfuscatedRange == null) {
+        if (rangeI.minifiedRange == null) {
           if (noLineRange == null) {
             // This is an "a() -> b" mapping (no concrete line numbers), remember this if there'll
             // be no better one.
             noLineRange = rangeI;
           }
-        } else if (rangeI.obfuscatedRange.contains(line)) {
-          // Concrete obfuscated range found ("x:y:a()[:u[:v]] -> b")
+        } else if (rangeI.minifiedRange.contains(line)) {
+          // Concrete minified range found ("x:y:a()[:u[:v]] -> b")
           int j = i + 1;
           for (; j < mappedRanges.size(); ++j) {
-            if (!Objects.equals(mappedRanges.get(j).obfuscatedRange, rangeI.obfuscatedRange)) {
+            if (!Objects.equals(mappedRanges.get(j).minifiedRange, rangeI.minifiedRange)) {
               break;
             }
           }
@@ -165,27 +166,27 @@
 
   /**
    * Mapping from the renamed signature to the naming information for a member.
-   * <p>
-   * A renamed signature is a signature where the member's name has been obfuscated but not the type
+   *
+   * <p>A renamed signature is a signature where the member's name has been renamed but not the type
    * information.
-   **/
+   */
   private final ImmutableMap<MethodSignature, MemberNaming> methodMembers;
   private final ImmutableMap<FieldSignature, MemberNaming> fieldMembers;
 
-  /** Map of obfuscated name -> MappedRangesOfName */
-  public final Map<String, MappedRangesOfName> mappedRangesByName;
+  /** Map of renamed name -> MappedRangesOfName */
+  public final Map<String, MappedRangesOfName> mappedRangesByRenamedName;
 
   private ClassNamingForNameMapper(
       String renamedName,
       String originalName,
       Map<MethodSignature, MemberNaming> methodMembers,
       Map<FieldSignature, MemberNaming> fieldMembers,
-      Map<String, MappedRangesOfName> mappedRangesByName) {
+      Map<String, MappedRangesOfName> mappedRangesByRenamedName) {
     this.renamedName = renamedName;
     this.originalName = originalName;
     this.methodMembers = ImmutableMap.copyOf(methodMembers);
     this.fieldMembers = ImmutableMap.copyOf(fieldMembers);
-    this.mappedRangesByName = mappedRangesByName;
+    this.mappedRangesByRenamedName = mappedRangesByRenamedName;
   }
 
   @Override
@@ -273,16 +274,13 @@
         });
 
     // Sort MappedRanges by sequence number to restore construction order (original Proguard-map
-    // input)
-    List<MappedRange> rangeList = new ArrayList<>();
-    for (MappedRangesOfName ranges : mappedRangesByName.values()) {
-      rangeList.addAll(ranges.mappedRanges);
+    // input).
+    List<MappedRange> mappedRangesSorted = new ArrayList<>();
+    for (MappedRangesOfName ranges : mappedRangesByRenamedName.values()) {
+      mappedRangesSorted.addAll(ranges.mappedRanges);
     }
-    rangeList.sort(
-        (lhs, rhs) -> {
-          return lhs.sequenceNumber - rhs.sequenceNumber;
-        });
-    for (MappedRange range : rangeList) {
+    mappedRangesSorted.sort(Comparator.comparingInt(range -> range.sequenceNumber));
+    for (MappedRange range : mappedRangesSorted) {
       writer.append("    ").append(range.toString()).append('\n');
     }
   }
@@ -313,7 +311,7 @@
         && renamedName.equals(that.renamedName)
         && methodMembers.equals(that.methodMembers)
         && fieldMembers.equals(that.fieldMembers)
-        && mappedRangesByName.equals(that.mappedRangesByName);
+        && mappedRangesByRenamedName.equals(that.mappedRangesByRenamedName);
   }
 
   @Override
@@ -322,27 +320,27 @@
     result = 31 * result + renamedName.hashCode();
     result = 31 * result + methodMembers.hashCode();
     result = 31 * result + fieldMembers.hashCode();
-    result = 31 * result + mappedRangesByName.hashCode();
+    result = 31 * result + mappedRangesByRenamedName.hashCode();
     return result;
   }
 
   /**
-   * MappedRange describes an (original line numbers, signature) <-> (obfuscated line numbers)
+   * MappedRange describes an (original line numbers, signature) <-> (minified line numbers)
    * mapping. It can describe 3 different things:
    *
    * <p>1. The method is renamed. The original source lines are preserved. The corresponding
    * Proguard-map syntax is "a(...) -> b"
    *
-   * <p>2. The source lines of a method in the original range are renumbered to the obfuscated
-   * range. In this case the {@link MappedRange#originalRange} is either a {@code Range} or null,
+   * <p>2. The source lines of a method in the original range are renumbered to the minified range.
+   * In this case the {@link MappedRange#originalRange} is either a {@code Range} or null,
    * indicating that the original range is unknown or is the same as the {@link
-   * MappedRange#obfuscatedRange}. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or
+   * MappedRange#minifiedRange}. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or
    * "x:y:a(...):u:v -> b"
    *
    * <p>3. The source line of a method is the inlining caller of the previous {@code MappedRange}.
    * In this case the {@link MappedRange@originalRange} is either an {@code int} or null, indicating
-   * that the original source line is unknown, or may be identical to a line of the obfuscated
-   * range. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or "x:y:a(...):u -> b"
+   * that the original source line is unknown, or may be identical to a line of the minified range.
+   * The corresponding Proguard-map syntax is "x:y:a(...) -> b" or "x:y:a(...):u -> b"
    */
   public static class MappedRange {
 
@@ -352,10 +350,10 @@
       return nextSequenceNumber++;
     }
 
-    public final Range obfuscatedRange; // Can be null, if so then originalRange must also be null.
+    public final Range minifiedRange; // Can be null, if so then originalRange must also be null.
     public final MethodSignature signature;
     public final Object originalRange; // null, Integer or Range.
-    public final String obfuscatedName;
+    public final String renamedName;
 
     /**
      * The sole purpose of {@link #sequenceNumber} is to preserve the order of members read from a
@@ -364,52 +362,49 @@
     private final int sequenceNumber = getNextSequenceNumber();
 
     private MappedRange(
-        Range obfuscatedRange,
-        MethodSignature signature,
-        Object originalRange,
-        String obfuscatedName) {
+        Range minifiedRange, MethodSignature signature, Object originalRange, String renamedName) {
 
-      assert obfuscatedRange != null || originalRange == null;
+      assert minifiedRange != null || originalRange == null;
       assert originalRange == null
           || originalRange instanceof Integer
           || originalRange instanceof Range;
 
-      this.obfuscatedRange = obfuscatedRange;
+      this.minifiedRange = minifiedRange;
       this.signature = signature;
       this.originalRange = originalRange;
-      this.obfuscatedName = obfuscatedName;
+      this.renamedName = renamedName;
     }
 
-    public int originalLineFromObfuscated(int obfuscatedLineNumber) {
-      if (obfuscatedRange == null) {
+    public int getOriginalLineNumber(int lineNumberAfterMinification) {
+      if (minifiedRange == null) {
         // General mapping without concrete line numbers: "a() -> b"
-        return obfuscatedLineNumber;
+        return lineNumberAfterMinification;
       }
-      assert obfuscatedRange.contains(obfuscatedLineNumber);
+      assert minifiedRange.contains(lineNumberAfterMinification);
       if (originalRange == null) {
         // Concrete identity mapping: "x:y:a() -> b"
-        return obfuscatedLineNumber;
+        return lineNumberAfterMinification;
       } else if (originalRange instanceof Integer) {
         // Inlinee: "x:y:a():u -> b"
         return (int) originalRange;
       } else {
         // "x:y:a():u:v -> b"
         assert originalRange instanceof Range;
-        return ((Range) originalRange).from + obfuscatedLineNumber - obfuscatedRange.from;
+        return ((Range) originalRange).from + lineNumberAfterMinification - minifiedRange.from;
       }
     }
 
     @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
-      if (obfuscatedRange != null) {
-        builder.append(obfuscatedRange).append(':');
+      if (minifiedRange != null) {
+        builder.append(minifiedRange).append(':');
       }
       builder.append(signature);
       if (originalRange != null) {
         builder.append(":").append(originalRange);
       }
-      builder.append(" -> ").append(obfuscatedName);
+      builder.append(" -> ").append(renamedName);
       return builder.toString();
     }
 
@@ -426,19 +421,19 @@
 
       MappedRange that = (MappedRange) o;
 
-      return Objects.equals(obfuscatedRange, that.obfuscatedRange)
+      return Objects.equals(minifiedRange, that.minifiedRange)
           && Objects.equals(originalRange, that.originalRange)
           && signature.equals(that.signature)
-          && obfuscatedName.equals(that.obfuscatedName);
+          && renamedName.equals(that.renamedName);
     }
 
     @Override
     public int hashCode() {
       // sequenceNumber is intentionally omitted from hashCode since it's not used in equality test.
-      int result = Objects.hashCode(obfuscatedRange);
+      int result = Objects.hashCode(minifiedRange);
       result = 31 * result + Objects.hashCode(originalRange);
       result = 31 * result + signature.hashCode();
-      result = 31 * result + obfuscatedName.hashCode();
+      result = 31 * result + renamedName.hashCode();
       return result;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/naming/MemberNaming.java b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
index c766c37..094dcaf 100644
--- a/src/main/java/com/android/tools/r8/naming/MemberNaming.java
+++ b/src/main/java/com/android/tools/r8/naming/MemberNaming.java
@@ -134,7 +134,12 @@
     }
 
     public static FieldSignature fromDexField(DexField field) {
-      return new FieldSignature(field.name.toSourceString(),
+      return fromDexField(field, false);
+    }
+
+    public static FieldSignature fromDexField(DexField field, boolean withQualifiedName) {
+      return new FieldSignature(
+          withQualifiedName ? field.qualifiedName() : field.name.toSourceString(),
           field.type.toSourceString());
     }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
index 6ba18ac..46fdc82 100644
--- a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
@@ -100,6 +100,17 @@
     }
 
     @Override
+    public DexField getRenamedFieldSignature(DexField originalField) {
+      return previousLense.getRenamedFieldSignature(originalField);
+    }
+
+    @Override
+    public DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
+      // TODO(b/79143143): implement this when re-enabling bridge analysis.
+      throw new Unimplemented("BridgeLense.getRenamedMethodSignature() not implemented");
+    }
+
+    @Override
     public DexType lookupType(DexType type) {
       return previousLense.lookupType(type);
     }
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 420af47..64387ce 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1672,15 +1672,15 @@
     /**
      * All methods that should be inlined if possible due to a configuration directive.
      */
-    public final Set<DexItem> alwaysInline;
+    public final Set<DexMethod> alwaysInline;
     /**
      * All methods that *must* be inlined due to a configuration directive (testing only).
      */
-    public final Set<DexItem> forceInline;
+    public final Set<DexMethod> forceInline;
     /**
      * All methods that *must* never be inlined due to a configuration directive (testing only).
      */
-    public final Set<DexItem> neverInline;
+    public final Set<DexMethod> neverInline;
     /**
      * All items with -identifiernamestring rule.
      * Bound boolean value indicates the rule is explicitly specified by users (<code>true</code>)
@@ -1829,8 +1829,8 @@
       this.assumedValues = previous.assumedValues;
       assert lense.assertNotModified(previous.alwaysInline);
       this.alwaysInline = previous.alwaysInline;
-      this.forceInline = previous.forceInline;
-      this.neverInline = previous.neverInline;
+      this.forceInline = rewriteMethodsWithRenamedSignature(previous.forceInline, lense);
+      this.neverInline = rewriteMethodsWithRenamedSignature(previous.neverInline, lense);
       this.identifierNameStrings =
           rewriteMixedItemsConservatively(previous.identifierNameStrings, lense);
       // Switchmap classes should never be affected by renaming.
@@ -1954,6 +1954,16 @@
       return builder.build();
     }
 
+    private static ImmutableSortedSet<DexMethod> rewriteMethodsWithRenamedSignature(
+        Set<DexMethod> methods, GraphLense lense) {
+      ImmutableSortedSet.Builder<DexMethod> builder =
+          new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
+      for (DexMethod method : methods) {
+        builder.add(lense.getRenamedMethodSignature(method));
+      }
+      return builder.build();
+    }
+
     private static ImmutableSortedSet<DexMethod> rewriteMethodsConservatively(
         Set<DexMethod> original, GraphLense lense) {
       ImmutableSortedSet.Builder<DexMethod> builder =
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 c95cfe9..473c990 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -59,9 +59,9 @@
   private final Set<ProguardConfigurationRule> rulesThatUseExtendsOrImplementsWrong =
       Sets.newIdentityHashSet();
   private final Set<DexItem> checkDiscarded = Sets.newIdentityHashSet();
-  private final Set<DexItem> alwaysInline = Sets.newIdentityHashSet();
-  private final Set<DexItem> forceInline = Sets.newIdentityHashSet();
-  private final Set<DexItem> neverInline = Sets.newIdentityHashSet();
+  private final Set<DexMethod> alwaysInline = Sets.newIdentityHashSet();
+  private final Set<DexMethod> forceInline = Sets.newIdentityHashSet();
+  private final Set<DexMethod> neverInline = Sets.newIdentityHashSet();
   private final Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking =
       new IdentityHashMap<>();
   private final Map<DexItem, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
@@ -727,13 +727,19 @@
     } else if (context instanceof InlineRule) {
       switch (((InlineRule) context).getType()) {
         case ALWAYS:
-          alwaysInline.add(item);
+          if (item instanceof DexEncodedMethod) {
+            alwaysInline.add(((DexEncodedMethod) item).method);
+          }
           break;
         case FORCE:
-          forceInline.add(item);
+          if (item instanceof DexEncodedMethod) {
+            forceInline.add(((DexEncodedMethod) item).method);
+          }
           break;
         case NEVER:
-          neverInline.add(item);
+          if (item instanceof DexEncodedMethod) {
+            neverInline.add(((DexEncodedMethod) item).method);
+          }
           break;
         default:
           throw new Unreachable();
@@ -755,9 +761,9 @@
     public final Set<DexItem> reasonAsked;
     public final Set<DexItem> keepPackageName;
     public final Set<DexItem> checkDiscarded;
-    public final Set<DexItem> alwaysInline;
-    public final Set<DexItem> forceInline;
-    public final Set<DexItem> neverInline;
+    public final Set<DexMethod> alwaysInline;
+    public final Set<DexMethod> forceInline;
+    public final Set<DexMethod> neverInline;
     public final Map<DexItem, ProguardMemberRule> noSideEffects;
     public final Map<DexItem, ProguardMemberRule> assumedValues;
     private final Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking;
@@ -792,9 +798,9 @@
         Set<DexItem> reasonAsked,
         Set<DexItem> keepPackageName,
         Set<DexItem> checkDiscarded,
-        Set<DexItem> alwaysInline,
-        Set<DexItem> forceInline,
-        Set<DexItem> neverInline,
+        Set<DexMethod> alwaysInline,
+        Set<DexMethod> forceInline,
+        Set<DexMethod> neverInline,
         Map<DexItem, ProguardMemberRule> noSideEffects,
         Map<DexItem, ProguardMemberRule> assumedValues,
         Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking,
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index ee596f3..a2dd383 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -252,7 +252,7 @@
     }
   }
 
-  private void extractPinnedItems(Iterable<DexItem> items, AbortReason reason) {
+  private <T extends DexItem> void extractPinnedItems(Iterable<T> items, AbortReason reason) {
     for (DexItem item : items) {
       if (item instanceof DexType || item instanceof DexClass) {
         DexType type = item instanceof DexType ? (DexType) item : ((DexClass) item).type;
@@ -1524,6 +1524,16 @@
     }
 
     @Override
+    public DexField getRenamedFieldSignature(DexField originalField) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
+      throw new Unreachable();
+    }
+
+    @Override
     public DexType lookupType(DexType type) {
       return type == source ? target : mergedClasses.getOrDefault(type, type);
     }
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 5163772..66490b2 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNaming;
@@ -35,6 +36,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 public class LineNumberOptimizer {
@@ -202,7 +204,10 @@
   }
 
   public static ClassNameMapper run(
-      DexApplication application, NamingLens namingLens, boolean identityMapping) {
+      DexApplication application,
+      GraphLense graphLense,
+      NamingLens namingLens,
+      boolean identityMapping) {
     ClassNameMapper.Builder classNameMapperBuilder = ClassNameMapper.builder();
     // Collect which files contain which classes that need to have their line numbers optimized.
     for (DexProgramClass clazz : application.classes()) {
@@ -212,8 +217,8 @@
         continue;
       }
 
-      IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
-          groupMethodsByName(namingLens, clazz);
+      IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByRenamedName =
+          groupMethodsByRenamedName(namingLens, clazz);
 
       // At this point we don't know if we really need to add this class to the builder.
       // It depends on whether any methods/fields are renamed or some methods contain positions.
@@ -230,10 +235,13 @@
       addClassToClassNaming(clazz, renamedClassName, onDemandClassNamingBuilder);
 
       // First transfer renamed fields to classNamingBuilder.
-      addFieldsToClassNaming(namingLens, clazz, onDemandClassNamingBuilder);
+      addFieldsToClassNaming(graphLense, namingLens, clazz, onDemandClassNamingBuilder);
 
-      // Then process the methods.
-      for (List<DexEncodedMethod> methods : methodsByName.values()) {
+      // Then process the methods, ordered by renamed name.
+      List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
+      renamedMethodNames.sort(DexString::slowCompareTo);
+      for (DexString methodName : renamedMethodNames) {
+        List<DexEncodedMethod> methods = methodsByRenamedName.get(methodName);
         if (methods.size() > 1) {
           // If there are multiple methods with the same name (overloaded) then sort them for
           // deterministic behaviour: the algorithm will assign new line numbers in this order.
@@ -258,7 +266,9 @@
             }
           }
 
-          MethodSignature originalSignature = MethodSignature.fromDexMethod(method.method);
+          DexMethod originalMethod = graphLense.getOriginalMethodSignature(method.method);
+          MethodSignature originalSignature =
+              MethodSignature.fromDexMethod(originalMethod, originalMethod.holder != clazz.type);
 
           DexString obfuscatedNameDexString = namingLens.lookupName(method.method);
           String obfuscatedName = obfuscatedNameDexString.toString();
@@ -266,7 +276,8 @@
           // Add simple "a() -> b" mapping if we won't have any other with concrete line numbers
           if (mappedPositions.isEmpty()) {
             // But only if it's been renamed.
-            if (obfuscatedNameDexString != method.method.name) {
+            if (obfuscatedNameDexString != originalMethod.name
+                || originalMethod.holder != clazz.type) {
               onDemandClassNamingBuilder
                   .get()
                   .addMappedRange(null, originalSignature, null, obfuscatedName);
@@ -275,7 +286,16 @@
           }
 
           Map<DexMethod, MethodSignature> signatures = new IdentityHashMap<>();
-          signatures.put(method.method, originalSignature);
+          signatures.put(originalMethod, originalSignature);
+          Function<DexMethod, MethodSignature> getOriginalMethodSignature =
+              m -> {
+                DexMethod original = graphLense.getOriginalMethodSignature(m);
+                return signatures.computeIfAbsent(
+                    original,
+                    key ->
+                        MethodSignature.fromDexMethod(
+                            original, original.holder != clazz.getType()));
+              };
 
           MemberNaming memberNaming = new MemberNaming(originalSignature, obfuscatedName);
           onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
@@ -306,23 +326,14 @@
             ClassNaming.Builder classNamingBuilder = onDemandClassNamingBuilder.get();
             classNamingBuilder.addMappedRange(
                 obfuscatedRange,
-                signatures.computeIfAbsent(
-                    firstPosition.method,
-                    m ->
-                        MethodSignature.fromDexMethod(
-                            m, firstPosition.method.holder != clazz.getType())),
+                getOriginalMethodSignature.apply(firstPosition.method),
                 originalRange,
                 obfuscatedName);
             Position caller = firstPosition.caller;
             while (caller != null) {
-              Position finalCaller = caller;
               classNamingBuilder.addMappedRange(
                   obfuscatedRange,
-                  signatures.computeIfAbsent(
-                      caller.method,
-                      m ->
-                          MethodSignature.fromDexMethod(
-                              m, finalCaller.method.holder != clazz.getType())),
+                  getOriginalMethodSignature.apply(caller.method),
                   Math.max(caller.line, 0), // Prevent against "no-position".
                   obfuscatedName);
               caller = caller.callerPosition;
@@ -378,42 +389,37 @@
     }
   }
 
-  private static void addFieldsToClassNaming(NamingLens namingLens, DexProgramClass clazz,
+  private static void addFieldsToClassNaming(
+      GraphLense graphLense,
+      NamingLens namingLens,
+      DexProgramClass clazz,
       Supplier<Builder> onDemandClassNamingBuilder) {
     clazz.forEachField(
         dexEncodedField -> {
           DexField dexField = dexEncodedField.field;
+          DexField originalField = graphLense.getOriginalFieldSignature(dexField);
           DexString renamedName = namingLens.lookupName(dexField);
-          if (renamedName != dexField.name) {
-            FieldSignature signature =
-                new FieldSignature(dexField.name.toString(), dexField.type.toString());
-            MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
+          if (renamedName != originalField.name || originalField.clazz != clazz.type) {
+            FieldSignature originalSignature =
+                FieldSignature.fromDexField(originalField, originalField.clazz != clazz.type);
+            MemberNaming memberNaming = new MemberNaming(originalSignature, renamedName.toString());
             onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
           }
         });
   }
 
-  private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByName(
+  private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByRenamedName(
       NamingLens namingLens, DexProgramClass clazz) {
-    IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
+    IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByRenamedName =
         new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
-    clazz.forEachMethod(
-        method -> {
-          // Add method only if renamed or contains positions.
-          if (namingLens.lookupName(method.method) != method.method.name
-              || doesContainPositions(method)) {
-            methodsByName.compute(
-                method.method.name,
-                (name, methods) -> {
-                  if (methods == null) {
-                    methods = new ArrayList<>();
-                  }
-                  methods.add(method);
-                  return methods;
-                });
-          }
-        });
-    return methodsByName;
+    for (DexEncodedMethod method : clazz.methods()) {
+      // Add method only if renamed or contains positions.
+      DexString renamedName = namingLens.lookupName(method.method);
+      if (renamedName != method.method.name || doesContainPositions(method)) {
+        methodsByRenamedName.computeIfAbsent(renamedName, key -> new ArrayList<>()).add(method);
+      }
+    }
+    return methodsByRenamedName;
   }
 
   private static boolean doesContainPositions(DexEncodedMethod method) {
diff --git a/src/test/examples/classmerging/ProguardFieldMapTest.java b/src/test/examples/classmerging/ProguardFieldMapTest.java
new file mode 100644
index 0000000..0cbf8a1
--- /dev/null
+++ b/src/test/examples/classmerging/ProguardFieldMapTest.java
@@ -0,0 +1,25 @@
+// 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 classmerging;
+
+public class ProguardFieldMapTest {
+
+  public static void main(String[] args) {
+    B b = new B();
+    b.test();
+  }
+
+  public static class A {
+
+    public String f = "A.f";
+  }
+
+  public static class B extends A {
+
+    public void test() {
+      System.out.println(f);
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/ProguardMethodMapTest.java b/src/test/examples/classmerging/ProguardMethodMapTest.java
new file mode 100644
index 0000000..9ce0ad7
--- /dev/null
+++ b/src/test/examples/classmerging/ProguardMethodMapTest.java
@@ -0,0 +1,29 @@
+// 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 classmerging;
+
+public class ProguardMethodMapTest {
+
+  public static void main(String[] args) {
+    B b = new B();
+    b.method();
+  }
+
+  public static class A {
+
+    public void method() {
+      System.out.println("In A.method()");
+    }
+  }
+
+  public static class B extends A {
+
+    @Override
+    public void method() {
+      System.out.println("In B.method()");
+      super.method();
+    }
+  }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 37a3010..3a70504 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -37,6 +37,12 @@
 -keep public class classmerging.PinnedParameterTypesTest$TestClass {
   public static void method(...);
 }
+-keep public class classmerging.ProguardFieldMapTest {
+  public static void main(...);
+}
+-keep public class classmerging.ProguardMethodMapTest {
+  public static void main(...);
+}
 -keep public class classmerging.SimpleInterfaceAccessTest {
   public static void main(...);
 }
diff --git a/src/test/examples/shaking1/print-mapping-cf.ref b/src/test/examples/shaking1/print-mapping-cf.ref
new file mode 100644
index 0000000..9bf8013
--- /dev/null
+++ b/src/test/examples/shaking1/print-mapping-cf.ref
@@ -0,0 +1,7 @@
+shaking1.Shaking -> shaking1.Shaking:
+shaking1.Used -> a.a:
+    java.lang.String name -> a
+    1:1:java.lang.String method():17:17 -> a
+    1:1:void main(java.lang.String[]):8:8 -> main
+    1:2:void <init>(java.lang.String):12:13 -> <init>
+    1:1:java.lang.String aMethodThatIsNotUsedButKept():21:21 -> aMethodThatIsNotUsedButKept
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index 6f3c97a..6322840 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,9 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.classmerging;
 
+import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.smali.SmaliBuilder.buildCode;
 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.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThat;
@@ -14,6 +16,7 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer.FileConsumer;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
@@ -21,6 +24,10 @@
 import com.android.tools.r8.VmTestRunner.IgnoreForRangeOfVmVersions;
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.MoveException;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import com.android.tools.r8.debug.DexDebugTestConfig;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -34,8 +41,11 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -97,7 +107,7 @@
   );
 
   @Test
-  public void testClassesHaveBeenMerged() throws Exception {
+  public void testClassesHaveBeenMerged() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
     // GenericInterface should be merged into GenericInterfaceImpl.
     for (String candidate : CAN_BE_MERGED) {
@@ -109,7 +119,7 @@
   }
 
   @Test
-  public void testClassesHaveNotBeenMerged() throws Exception {
+  public void testClassesHaveNotBeenMerged() throws Throwable {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
       assertTrue(inspector.clazz(candidate).isPresent());
@@ -124,7 +134,7 @@
   // Assuming that the chance of observing one of the two orderings is 50%, this test therefore has
   // approximately 3% chance of succeeding although there is an issue.
   @Test
-  public void testCallGraphCycle() throws Exception {
+  public void testCallGraphCycle() throws Throwable {
     String main = "classmerging.CallGraphCycleTest";
     Path[] programFiles =
         new Path[] {
@@ -140,7 +150,7 @@
   }
 
   @Test
-  public void testConflictInGeneratedName() throws Exception {
+  public void testConflictInGeneratedName() throws Throwable {
     String main = "classmerging.ConflictInGeneratedNameTest";
     Path[] programFiles =
         new Path[] {
@@ -162,7 +172,9 @@
               configure(options);
               // Avoid that direct methods in B get inlined.
               options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
-            });
+            },
+            // Disable debug testing since the test has a method with "$classmerging$" in the name.
+            null);
 
     ClassSubject clazzSubject = inspector.clazz("classmerging.ConflictInGeneratedNameTest$B");
     assertThat(clazzSubject, isPresent());
@@ -204,14 +216,14 @@
   }
 
   @Test
-  public void testConflictWasDetected() throws Exception {
+  public void testConflictWasDetected() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
     assertTrue(inspector.clazz("classmerging.ConflictingInterface").isPresent());
     assertTrue(inspector.clazz("classmerging.ConflictingInterfaceImpl").isPresent());
   }
 
   @Test
-  public void testFieldCollision() throws Exception {
+  public void testFieldCollision() throws Throwable {
     String main = "classmerging.FieldCollisionTest";
     Path[] programFiles =
         new Path[] {
@@ -227,7 +239,7 @@
   }
 
   @Test
-  public void testLambdaRewriting() throws Exception {
+  public void testLambdaRewriting() throws Throwable {
     String main = "classmerging.LambdaRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -250,7 +262,7 @@
   }
 
   @Test
-  public void testMethodCollision() throws Exception {
+  public void testMethodCollision() throws Throwable {
     String main = "classmerging.MethodCollisionTest";
     Path[] programFiles =
         new Path[] {
@@ -275,7 +287,7 @@
   }
 
   @Test
-  public void testNestedDefaultInterfaceMethodsTest() throws Exception {
+  public void testNestedDefaultInterfaceMethodsTest() throws Throwable {
     String main = "classmerging.NestedDefaultInterfaceMethodsTest";
     Path[] programFiles =
         new Path[] {
@@ -294,7 +306,7 @@
   }
 
   @Test
-  public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Exception {
+  public void testNestedDefaultInterfaceMethodsWithCDumpTest() throws Throwable {
     String main = "classmerging.NestedDefaultInterfaceMethodsTest";
     Path[] programFiles =
         new Path[] {
@@ -319,7 +331,7 @@
   }
 
   @Test
-  public void testPinnedParameterTypes() throws Exception {
+  public void testPinnedParameterTypes() throws Throwable {
     String main = "classmerging.PinnedParameterTypesTest";
     Path[] programFiles =
         new Path[] {
@@ -342,7 +354,210 @@
   }
 
   @Test
-  public void testSuperCallWasDetected() throws Exception {
+  public void testProguardFieldMap() throws Throwable {
+    String main = "classmerging.ProguardFieldMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardFieldMapTest.class"),
+          CF_DIR.resolve("ProguardFieldMapTest$A.class"),
+          CF_DIR.resolve("ProguardFieldMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardFieldMapTest -> classmerging.ProguardFieldMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardFieldMapTest$A -> classmerging.ProguardFieldMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "    2:2:void <init>():16:16 -> <init>",
+                "classmerging.ProguardFieldMapTest$B -> classmerging.ProguardFieldMapTest$B:",
+                "    1:1:void <init>():19:19 -> <init>",
+                "    1:1:void test():22:22 -> test");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardFieldMapTest -> classmerging.ProguardFieldMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardFieldMapTest$B -> classmerging.ProguardFieldMapTest$B:",
+                "    java.lang.String classmerging.ProguardFieldMapTest$A.f -> f",
+                "    1:1:void classmerging.ProguardFieldMapTest$A.<init>():14:14 -> <init>",
+                "    1:1:void <init>():19 -> <init>",
+                "    2:2:void classmerging.ProguardFieldMapTest$A.<init>():16:16 -> <init>",
+                "    2:2:void <init>():19 -> <init>",
+                "    1:1:void test():22:22 -> test");
+    Set<String> preservedClassNames =
+        ImmutableSet.of("classmerging.ProguardFieldMapTest", "classmerging.ProguardFieldMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  @Test
+  public void testProguardMethodMap() throws Throwable {
+    String main = "classmerging.ProguardMethodMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardMethodMapTest.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$A.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$A -> classmerging.ProguardMethodMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "    1:1:void method():17:17 -> method",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void <init>():21:21 -> <init>",
+                "    1:2:void method():25:26 -> method");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                // TODO(christofferqa): Should this be "...<init>():14 -> <init>" to reflect that
+                // A.<init> has been inlined into B.<init>?
+                "    1:1:void classmerging.ProguardMethodMapTest$A.<init>():14:14 -> <init>",
+                // TODO(christofferqa): Should this be " ...<init>():21:21 -> <init>"?
+                "    1:1:void <init>():21 -> <init>",
+                "    1:2:void method():25:26 -> method",
+                "    1:1:void classmerging.ProguardMethodMapTest$A.method():17:17 -> "
+                    + "method$classmerging$ProguardMethodMapTest$A");
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(EXAMPLE_KEEP),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  @Test
+  public void testProguardMethodMapAfterInlining() throws Throwable {
+    String main = "classmerging.ProguardMethodMapTest";
+    Path[] programFiles =
+        new Path[] {
+          CF_DIR.resolve("ProguardMethodMapTest.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$A.class"),
+          CF_DIR.resolve("ProguardMethodMapTest$B.class")
+        };
+
+    // Try without vertical class merging, to check that output is as expected.
+    String expectedProguardMapWithoutClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$A -> classmerging.ProguardMethodMapTest$A:",
+                "    1:1:void <init>():14:14 -> <init>",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void <init>():21:21 -> <init>",
+                "    1:1:void method():25:25 -> method",
+                "    2:2:void classmerging.ProguardMethodMapTest$A.method():17:17 -> method",
+                "    2:2:void method():26 -> method");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        Predicates.alwaysTrue(),
+        getProguardConfig(
+            EXAMPLE_KEEP,
+            "-forceinline class classmerging.ProguardMethodMapTest$A { public void method(); }"),
+        options -> {
+          configure(options);
+          options.enableClassMerging = false;
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithoutClassMerging));
+        });
+
+    // Try with vertical class merging.
+    String expectedProguardMapWithClassMerging =
+        Joiner.on(System.lineSeparator())
+            .join(
+                "classmerging.ProguardMethodMapTest -> classmerging.ProguardMethodMapTest:",
+                "    1:2:void main(java.lang.String[]):10:11 -> main",
+                "classmerging.ProguardMethodMapTest$B -> classmerging.ProguardMethodMapTest$B:",
+                "    1:1:void classmerging.ProguardMethodMapTest$A.<init>():14:14 -> <init>",
+                "    1:1:void <init>():21 -> <init>",
+                "    1:1:void method():25:25 -> method",
+                "    2:2:void classmerging.ProguardMethodMapTest$A.method():17:17 -> method",
+                "    2:2:void method():26 -> method");
+    Set<String> preservedClassNames =
+        ImmutableSet.of(
+            "classmerging.ProguardMethodMapTest", "classmerging.ProguardMethodMapTest$B");
+    runTestOnInput(
+        main,
+        readProgramFiles(programFiles),
+        preservedClassNames::contains,
+        getProguardConfig(
+            EXAMPLE_KEEP,
+            "-forceinline class classmerging.ProguardMethodMapTest$A { public void method(); }"),
+        options -> {
+          configure(options);
+          options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+          options.proguardMapConsumer =
+              (proguardMap, handler) ->
+                  assertThat(proguardMap, containsString(expectedProguardMapWithClassMerging));
+        });
+  }
+
+  @Test
+  public void testSuperCallWasDetected() throws Throwable {
     String main = "classmerging.SuperCallRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -366,7 +581,7 @@
   // targets SubClassThatReferencesSuperMethod.referencedMethod. When running without class
   // merging, R8 should not rewrite the invoke-super instruction into invoke-direct.
   @Test
-  public void testSuperCallNotRewrittenToDirect() throws Exception {
+  public void testSuperCallNotRewrittenToDirect() throws Throwable {
     String main = "classmerging.SuperCallRewritingTest";
     Path[] programFiles =
         new Path[] {
@@ -446,7 +661,7 @@
   //   }
   @Test
   @IgnoreForRangeOfVmVersions(from = Version.V5_1_1, to = Version.V6_0_1)
-  public void testSuperCallToMergedClassIsRewritten() throws Exception {
+  public void testSuperCallToMergedClassIsRewritten() throws Throwable {
     String main = "classmerging.SuperCallToMergedClassIsRewrittenTest";
     Set<String> preservedClassNames =
         ImmutableSet.of(
@@ -569,7 +784,7 @@
   }
 
   @Test
-  public void testConflictingInterfaceSignatures() throws Exception {
+  public void testConflictingInterfaceSignatures() throws Throwable {
     String main = "classmerging.ConflictingInterfaceSignaturesTest";
     Path[] programFiles =
         new Path[] {
@@ -588,7 +803,7 @@
   // If an exception class A is merged into another exception class B, then all exception tables
   // should be updated, and class A should be removed entirely.
   @Test
-  public void testExceptionTables() throws Exception {
+  public void testExceptionTables() throws Throwable {
     String main = "classmerging.ExceptionTest";
     Path[] programFiles =
         new Path[] {
@@ -624,7 +839,7 @@
   }
 
   @Test
-  public void testMergeDefaultMethodIntoClass() throws Exception {
+  public void testMergeDefaultMethodIntoClass() throws Throwable {
     String main = "classmerging.MergeDefaultMethodIntoClassTest";
     Path[] programFiles =
         new Path[] {
@@ -658,7 +873,7 @@
   }
 
   @Test
-  public void testNativeMethod() throws Exception {
+  public void testNativeMethod() throws Throwable {
     String main = "classmerging.ClassWithNativeMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -676,7 +891,7 @@
   }
 
   @Test
-  public void testNoIllegalClassAccess() throws Exception {
+  public void testNoIllegalClassAccess() throws Throwable {
     String main = "classmerging.SimpleInterfaceAccessTest";
     Path[] programFiles =
         new Path[] {
@@ -701,7 +916,7 @@
   }
 
   @Test
-  public void testNoIllegalClassAccessWithAccessModifications() throws Exception {
+  public void testNoIllegalClassAccessWithAccessModifications() throws Throwable {
     // If access modifications are allowed then SimpleInterface should be merged into
     // SimpleInterfaceImpl.
     String main = "classmerging.SimpleInterfaceAccessTest";
@@ -736,7 +951,7 @@
   // into an invoke-direct instruction after it gets merged into class C. We should add a test that
   // checks that this works with and without inlining of the method B.m().
   @Test
-  public void testRewritePinnedMethod() throws Exception {
+  public void testRewritePinnedMethod() throws Throwable {
     String main = "classmerging.RewritePinnedMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -759,7 +974,7 @@
   }
 
   @Test
-  public void testTemplateMethodPattern() throws Exception {
+  public void testTemplateMethodPattern() throws Throwable {
     String main = "classmerging.TemplateMethodTest";
     Path[] programFiles =
         new Path[] {
@@ -774,7 +989,7 @@
   }
 
   private CodeInspector runTest(
-      String main, Path[] programFiles, Predicate<String> preservedClassNames) throws Exception {
+      String main, Path[] programFiles, Predicate<String> preservedClassNames) throws Throwable {
     return runTest(main, programFiles, preservedClassNames, getProguardConfig(EXAMPLE_KEEP));
   }
 
@@ -783,14 +998,14 @@
       Path[] programFiles,
       Predicate<String> preservedClassNames,
       String proguardConfig)
-      throws Exception {
+      throws Throwable {
     return runTestOnInput(
         main, readProgramFiles(programFiles), preservedClassNames, proguardConfig);
   }
 
   private CodeInspector runTestOnInput(
       String main, AndroidApp input, Predicate<String> preservedClassNames, String proguardConfig)
-      throws Exception {
+      throws Throwable {
     return runTestOnInput(main, input, preservedClassNames, proguardConfig, this::configure);
   }
 
@@ -800,8 +1015,37 @@
       Predicate<String> preservedClassNames,
       String proguardConfig,
       Consumer<InternalOptions> optionsConsumer)
-      throws Exception {
-    AndroidApp output = compileWithR8(input, proguardConfig, optionsConsumer);
+      throws Throwable {
+    return runTestOnInput(
+        main,
+        input,
+        preservedClassNames,
+        proguardConfig,
+        optionsConsumer,
+        new VerticalClassMergerDebugTest(main));
+  }
+
+  private CodeInspector runTestOnInput(
+      String main,
+      AndroidApp input,
+      Predicate<String> preservedClassNames,
+      String proguardConfig,
+      Consumer<InternalOptions> optionsConsumer,
+      VerticalClassMergerDebugTest debugTestRunner)
+      throws Throwable {
+    Path proguardMapPath = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath();
+    R8Command.Builder commandBuilder =
+        ToolHelper.prepareR8CommandBuilder(input)
+            .addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
+    ToolHelper.allowTestProguardOptions(commandBuilder);
+    AndroidApp output =
+        ToolHelper.runR8(
+            commandBuilder.build(),
+            options -> {
+              optionsConsumer.accept(options);
+              options.proguardMapConsumer =
+                  new FileConsumer(proguardMapPath, options.proguardMapConsumer);
+            });
     CodeInspector inputInspector = new CodeInspector(input);
     CodeInspector outputInspector = new CodeInspector(output);
     // Check that all classes in [preservedClassNames] are in fact preserved.
@@ -815,6 +1059,11 @@
     }
     // Check that the R8-generated code produces the same result as D8-generated code.
     assertEquals(runOnArt(compileWithD8(input), main), runOnArt(output, main));
+    // Check that we never come across a method that has a name with "$classmerging$" in it during
+    // debugging.
+    if (debugTestRunner != null) {
+      debugTestRunner.test(output, proguardMapPath);
+    }
     return outputInspector;
   }
 
@@ -830,4 +1079,82 @@
     }
     return builder.toString();
   }
+
+  private class VerticalClassMergerDebugTest extends DebugTestBase {
+
+    private final String main;
+    private DebugTestRunner runner = null;
+
+    public VerticalClassMergerDebugTest(String main) {
+      this.main = main;
+    }
+
+    public void test(AndroidApp app, Path proguardMapPath) throws Throwable {
+      Path appPath =
+          File.createTempFile("app", ".zip", ClassMergingTest.this.temp.getRoot()).toPath();
+      app.writeToZip(appPath, OutputMode.DexIndexed);
+
+      DexDebugTestConfig config = new DexDebugTestConfig(appPath);
+      config.allowUnprocessedCommands();
+      config.setProguardMap(proguardMapPath);
+
+      this.runner =
+          getDebugTestRunner(
+              config, main, breakpoint(main, "main"), run(), stepIntoUntilNoLongerInApp());
+      this.runner.runBare();
+    }
+
+    private void checkState(DebuggeeState state) {
+      // If a class pkg.A is merged into pkg.B, and a method pkg.A.m() needs to be renamed, then
+      // it will be renamed to pkg.B.m$pkg$A(). Since all tests are in the package "classmerging",
+      // we check that no methods in the debugging state (i.e., after the Proguard map has been
+      // applied) contain "$classmerging$.
+      String qualifiedMethodSignature =
+          state.getClassSignature() + "->" + state.getMethodName() + state.getMethodSignature();
+      boolean holderIsCompanionClass = state.getClassName().endsWith(COMPANION_CLASS_NAME_SUFFIX);
+      if (!holderIsCompanionClass) {
+        assertThat(qualifiedMethodSignature, not(containsString("$classmerging$")));
+      }
+    }
+
+    // Keeps stepping in until it is no longer in a class from the classmerging package.
+    // Then starts stepping out until it is again in the classmerging package.
+    private Command stepIntoUntilNoLongerInApp() {
+      return stepUntil(
+          StepKind.INTO,
+          StepLevel.INSTRUCTION,
+          state -> {
+            if (state.getClassSignature().contains("classmerging")) {
+              checkState(state);
+
+              // Continue stepping into.
+              return false;
+            }
+
+            // Stop stepping into.
+            runner.enqueueCommandFirst(stepOutUntilInApp());
+            return true;
+          });
+    }
+
+    // Keeps stepping out until it is in a class from the classmerging package.
+    // Then starts stepping in until it is no longer in the classmerging package.
+    private Command stepOutUntilInApp() {
+      return stepUntil(
+          StepKind.OUT,
+          StepLevel.INSTRUCTION,
+          state -> {
+            if (state.getClassSignature().contains("classmerging")) {
+              checkState(state);
+
+              // Stop stepping out.
+              runner.enqueueCommandFirst(stepIntoUntilNoLongerInApp());
+              return true;
+            }
+
+            // Continue stepping out.
+            return false;
+          });
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 5707812..e9ef5ed 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -137,6 +137,18 @@
   private void runInternal(
       DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
       throws Throwable {
+    getDebugTestRunner(config, debuggeeClass, commands).runBare();
+  }
+
+  protected DebugTestRunner getDebugTestRunner(
+      DebugTestConfig config, String debuggeeClass, JUnit3Wrapper.Command... commands)
+      throws Throwable {
+    return getDebugTestRunner(config, debuggeeClass, Arrays.asList(commands));
+  }
+
+  protected DebugTestRunner getDebugTestRunner(
+      DebugTestConfig config, String debuggeeClass, List<JUnit3Wrapper.Command> commands)
+      throws Throwable {
     // Skip test due to unsupported runtime.
     Assume.assumeTrue("Skipping test " + testName.getMethodName() + " because ART is not supported",
         ToolHelper.artSupported());
@@ -152,7 +164,7 @@
             ? null
             : ClassNameMapper.mapperFromFile(config.getProguardMap());
 
-    new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper).runBare();
+    return new JUnit3Wrapper(config, debuggeeClass, commands, classNameMapper);
   }
 
   /**
@@ -495,8 +507,14 @@
     return t -> inspector.accept(t.debuggeeState.getLocalValues().get(localName));
   }
 
+  protected interface DebugTestRunner {
+    void enqueueCommandFirst(JUnit3Wrapper.Command command);
+
+    void runBare() throws Throwable;
+  }
+
   @Ignore("Prevents Gradle from running the wrapper as a test.")
-  static class JUnit3Wrapper extends JDWPTestCase {
+  public static class JUnit3Wrapper extends JDWPTestCase implements DebugTestRunner {
 
     private final String debuggeeClassName;
 
@@ -630,7 +648,7 @@
           return null;
         }
         ClassNamingForNameMapper.MappedRangesOfName ranges =
-            classNaming.mappedRangesByName.get(obfuscatedMethodName);
+            classNaming.mappedRangesByRenamedName.get(obfuscatedMethodName);
         if (ranges == null) {
           return null;
         }
@@ -683,7 +701,7 @@
         if (range == null) {
           return obfuscatedLineNumber;
         }
-        return range.originalLineFromObfuscated(obfuscatedLineNumber);
+        return range.getOriginalLineNumber(obfuscatedLineNumber);
       }
 
       @Override
@@ -694,7 +712,7 @@
           return null;
         }
         ClassNamingForNameMapper.MappedRangesOfName ranges =
-            classNaming.mappedRangesByName.get(obfuscatedMethodName);
+            classNaming.mappedRangesByRenamedName.get(obfuscatedMethodName);
         if (ranges == null) {
           return null;
         }
@@ -706,8 +724,7 @@
         for (MappedRange range : mappedRanges) {
           lines.add(
               new SignatureAndLine(
-                  range.signature.toString(),
-                  range.originalLineFromObfuscated(obfuscatedLineNumber)));
+                  range.signature.toString(), range.getOriginalLineNumber(obfuscatedLineNumber)));
         }
         return lines;
       }
@@ -801,8 +818,10 @@
         // Continue stepping until mainLoopStep exits with false.
       }
 
-      assertTrue(
-          "All commands have NOT been processed for config: " + config, commandsQueue.isEmpty());
+      if (config.mustProcessAllCommands()) {
+        assertTrue(
+            "All commands have NOT been processed for config: " + config, commandsQueue.isEmpty());
+      }
 
       logWriter.println("Finish loop");
     }
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
index 556e17f..519b580 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
@@ -17,6 +17,7 @@
     DEX
   }
 
+  private boolean mustProcessAllCommands = true;
   private List<Path> paths = new ArrayList<>();
 
   private Path proguardMap = null;
@@ -47,6 +48,14 @@
     return this;
   }
 
+  public boolean mustProcessAllCommands() {
+    return mustProcessAllCommands;
+  }
+
+  public void allowUnprocessedCommands() {
+    mustProcessAllCommands = false;
+  }
+
   /** Proguard map that the debuggee has been translated according to, null if not present. */
   public Path getProguardMap() {
     return proguardMap;
diff --git a/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
index 0f8ac65..ce84f51 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
@@ -185,7 +185,7 @@
     ClassNamingForNameMapper classNaming = mapper.getClassNaming(TEST_PACKAGE + "." + TEST_CLASS);
     assertNotNull(classNaming);
 
-    MappedRangesOfName rangesForMain = classNaming.mappedRangesByName.get("main");
+    MappedRangesOfName rangesForMain = classNaming.mappedRangesByRenamedName.get("main");
     assertNotNull(rangesForMain);
 
     List<MappedRange> frames = rangesForMain.allRangesForLine(expectedLineNumber);
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
index abc7a73..6ae151e 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreDeterministicTest.java
@@ -3,13 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.internal;
 
+import static junit.framework.TestCase.assertEquals;
+
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.beust.jcommander.internal.Lists;
@@ -20,52 +21,60 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 public class R8GMSCoreDeterministicTest extends GMSCoreCompilationTestBase {
 
-  public Set<DexEncodedMethod> shuffle(Set<DexEncodedMethod> methods) {
+  private static class CompilationResult {
+    AndroidApp app;
+    String proguardMap;
+  }
+
+  private static Set<DexEncodedMethod> shuffle(Set<DexEncodedMethod> methods) {
     List<DexEncodedMethod> toShuffle = Lists.newArrayList(methods);
     Collections.shuffle(toShuffle);
     return new LinkedHashSet<>(toShuffle);
   }
 
-  private AndroidApp doRun()
-      throws IOException, ProguardRuleParserException, ExecutionException,
-      CompilationFailedException {
+  private CompilationResult doRun() throws IOException, CompilationFailedException {
     R8Command command =
         R8Command.builder()
             .addProgramFiles(Paths.get(GMSCORE_V7_DIR, GMSCORE_APK))
             .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
             .setMinApiLevel(AndroidApiLevel.L.getLevel())
             .build();
-    return ToolHelper.runR8(
-        command,
-        options -> {
-          // For this test just do random shuffle.
-          options.testing.irOrdering = this::shuffle;
-          // Only use one thread to process to process in the order decided by the callback.
-          options.numberOfThreads = 1;
-          // Ignore the missing classes.
-          options.ignoreMissingClasses = true;
-        });
+    CompilationResult result = new CompilationResult();
+    result.app =
+        ToolHelper.runR8(
+            command,
+            options -> {
+              // For this test just do random shuffle.
+              options.testing.irOrdering = R8GMSCoreDeterministicTest::shuffle;
+              // Only use one thread to process to process in the order decided by the callback.
+              options.numberOfThreads = 1;
+              // Ignore the missing classes.
+              options.ignoreMissingClasses = true;
+              // Store the generated Proguard map.
+              options.proguardMapConsumer =
+                  (proguardMap, handler) -> result.proguardMap = proguardMap;
+            });
+    return result;
   }
 
   @Test
   public void deterministic() throws Exception {
-
     // Run two independent compilations.
-    AndroidApp app1 = doRun();
-    AndroidApp app2 = doRun();
+    CompilationResult result1 = doRun();
+    CompilationResult result2 = doRun();
 
     // Check that the generated bytecode runs through the dex2oat verifier with no errors.
     Path combinedInput = temp.getRoot().toPath().resolve("all.jar");
     Path oatFile = temp.getRoot().toPath().resolve("all.oat");
-    app1.writeToZip(combinedInput, OutputMode.DexIndexed);
+    result1.app.writeToZip(combinedInput, OutputMode.DexIndexed);
     ToolHelper.runDex2Oat(combinedInput, oatFile);
 
     // Verify that the result of the two compilations was the same.
-    assertIdenticalApplications(app1, app2);
+    assertIdenticalApplications(result1.app, result2.app);
+    assertEquals(result1.proguardMap, result2.proguardMap);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
index 6adf680..d881185 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10DeployJarVerificationTest.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.internal;
 
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
 import com.android.tools.r8.utils.AndroidApp;
@@ -11,17 +13,33 @@
 
 public class R8GMSCoreV10DeployJarVerificationTest extends GMSCoreDeployJarVerificationTest {
 
+  private String proguardMap1 = null;
+  private String proguardMap2 = null;
+
   @Test
   public void buildFromDeployJar() throws Exception {
     // TODO(tamaskenez): set hasReference = true when we have the noshrink file for V10
-    AndroidApp app1 = buildFromDeployJar(
-        CompilerUnderTest.R8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
-    AndroidApp app2 = buildFromDeployJar(
-        CompilerUnderTest.R8, CompilationMode.RELEASE,
-        GMSCoreCompilationTestBase.GMSCORE_V10_DIR, false);
+    AndroidApp app1 =
+        buildFromDeployJar(
+            CompilerUnderTest.R8,
+            CompilationMode.RELEASE,
+            GMSCoreCompilationTestBase.GMSCORE_V10_DIR,
+            false,
+            options ->
+                options.proguardMapConsumer =
+                    (proguardMap, handler) -> this.proguardMap1 = proguardMap);
+    AndroidApp app2 =
+        buildFromDeployJar(
+            CompilerUnderTest.R8,
+            CompilationMode.RELEASE,
+            GMSCoreCompilationTestBase.GMSCORE_V10_DIR,
+            false,
+            options ->
+                options.proguardMapConsumer =
+                    (proguardMap, handler) -> this.proguardMap2 = proguardMap);
 
     // Verify that the result of the two compilations was the same.
     assertIdenticalApplications(app1, app2);
+    assertEquals(proguardMap1, proguardMap2);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
index ae470c6..4374d0e 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreV10TreeShakeJarVerificationTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.internal;
 
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.utils.AndroidApp;
 import org.junit.Test;
@@ -10,15 +12,33 @@
 public class R8GMSCoreV10TreeShakeJarVerificationTest
     extends R8GMSCoreTreeShakeJarVerificationTest {
 
+  private String proguardMap1 = null;
+  private String proguardMap2 = null;
+
   @Test
   public void buildAndTreeShakeFromDeployJar() throws Exception {
     // TODO(tamaskenez): set hasReference = true when we have the noshrink file for V10
-    AndroidApp app1 = buildAndTreeShakeFromDeployJar(
-        CompilationMode.RELEASE, GMSCORE_V10_DIR, false, GMSCORE_V10_MAX_SIZE, null);
-    AndroidApp app2 = buildAndTreeShakeFromDeployJar(
-        CompilationMode.RELEASE, GMSCORE_V10_DIR, false, GMSCORE_V10_MAX_SIZE, null);
+    AndroidApp app1 =
+        buildAndTreeShakeFromDeployJar(
+            CompilationMode.RELEASE,
+            GMSCORE_V10_DIR,
+            false,
+            GMSCORE_V10_MAX_SIZE,
+            options ->
+                options.proguardMapConsumer =
+                    (proguardMap, handler) -> this.proguardMap1 = proguardMap);
+    AndroidApp app2 =
+        buildAndTreeShakeFromDeployJar(
+            CompilationMode.RELEASE,
+            GMSCORE_V10_DIR,
+            false,
+            GMSCORE_V10_MAX_SIZE,
+            options ->
+                options.proguardMapConsumer =
+                    (proguardMap, handler) -> this.proguardMap2 = proguardMap);
 
     // Verify that the result of the two compilations was the same.
     assertIdenticalApplications(app1, app2);
+    assertEquals(proguardMap1, proguardMap2);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
index 3421cfe..911ceb7 100644
--- a/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b69825683/Regress69825683Test.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
@@ -16,10 +17,25 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class Regress69825683Test extends TestBase {
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public Regress69825683Test(Backend backend) {
+    this.backend = backend;
+  }
 
   @Test
   public void outerConstructsInner() throws Exception {
@@ -32,9 +48,18 @@
         "}",
         "-dontobfuscate"),
         Origin.unknown());
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    if (backend == Backend.DEX) {
+      builder
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder
+          .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
-    CodeInspector inspector = new CodeInspector(app);
+    CodeInspector inspector = new CodeInspector(app, o -> o.enableCfFrontend = true);
     List<FoundClassSubject> classes = inspector.allClasses();
 
     // Check that the synthetic class is still present.
@@ -51,7 +76,9 @@
     String innerName = innerClass.getCanonicalName();
     int index = innerName.lastIndexOf('.');
     innerName = innerName.substring(0, index) + "$" + innerName.substring(index + 1);
-    assertTrue(runOnArt(app, mainClass).startsWith(innerName));
+    assertTrue(
+        (backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass))
+            .startsWith(innerName));
   }
 
   @Test
@@ -65,9 +92,18 @@
         "}",
         "-dontobfuscate"),
         Origin.unknown());
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    if (backend == Backend.DEX) {
+      builder
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder
+          .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableClassInlining = false);
-    CodeInspector inspector = new CodeInspector(app);
+    CodeInspector inspector = new CodeInspector(app, o -> o.enableCfFrontend = true);
     List<FoundClassSubject> classes = inspector.allClasses();
 
     // Check that the synthetic class is still present.
@@ -79,6 +115,8 @@
             .count());
 
     // Run code to check that the constructor with synthetic class as argument is present.
-    assertTrue(runOnArt(app, mainClass).startsWith(mainClass.getCanonicalName()));
+    assertTrue(
+        (backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass))
+            .startsWith(mainClass.getCanonicalName()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b71604169/Regress71604169Test.java b/src/test/java/com/android/tools/r8/regress/b71604169/Regress71604169Test.java
index f40ecfd..cc356c3 100644
--- a/src/test/java/com/android/tools/r8/regress/b71604169/Regress71604169Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b71604169/Regress71604169Test.java
@@ -6,15 +6,32 @@
 
 import static junit.framework.TestCase.assertEquals;
 
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
 import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class Regress71604169Test extends TestBase {
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public Regress71604169Test(Backend backend) {
+    this.backend = backend;
+  }
+
   @Test
   public void test() throws Exception {
     R8Command.Builder builder = R8Command.builder();
@@ -28,7 +45,20 @@
     builder.addProguardConfiguration(
         ImmutableList.of(keepMainProguardConfiguration(mainClass, true, false)), Origin.unknown());
 
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    assertEquals("Hello, world!", runOnArt(ToolHelper.runR8(builder.build()), mainClass));
+    if (backend == Backend.DEX) {
+      builder
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder
+          .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
+    assertEquals(
+        "Hello, world!",
+        backend == Backend.DEX
+            ? runOnArt(ToolHelper.runR8(builder.build()), mainClass)
+            : runOnJava(ToolHelper.runR8(builder.build()), mainClass));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index cba8686..ad67919 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -14,12 +14,15 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.junit.Assert;
@@ -27,8 +30,28 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class TreeShakingSpecificTest {
+  enum Backend {
+    DEX,
+    CF
+  }
+
+  private Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public TreeShakingSpecificTest(Backend backend) {
+    this.backend = backend;
+  }
+
   private static final String VALID_PROGUARD_DIR = "src/test/proguard/valid/";
 
   @Rule
@@ -37,18 +60,28 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  private void finishBuild(R8Command.Builder builder, Path out, String test) throws IOException {
+    Path input;
+    if (backend == Backend.DEX) {
+      builder.setOutput(out, OutputMode.DexIndexed);
+      input = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
+    } else {
+      builder.setOutput(out, OutputMode.ClassFile);
+      input = Paths.get(EXAMPLES_BUILD_DIR, test + ".jar");
+    }
+    ToolHelper.getAppBuilder(builder).addProgramFiles(input);
+  }
+
   @Test
   public void testIgnoreWarnings() throws Exception {
     // Generate R8 processed version without library option.
     Path out = temp.getRoot().toPath();
     String test = "shaking2";
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
     Path ignoreWarnings = Paths.get(VALID_PROGUARD_DIR, "ignorewarnings.flags");
     R8Command.Builder builder = R8Command.builder()
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules, ignoreWarnings);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
+    finishBuild(builder, out, test);
     R8.run(builder.build());
   }
 
@@ -57,7 +90,6 @@
     // Generate R8 processed version without library option.
     Path out = temp.getRoot().toPath();
     String test = "shaking2";
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
     DiagnosticsHandler handler = new DiagnosticsHandler() {
       @Override
@@ -68,9 +100,8 @@
       }
     };
     R8Command.Builder builder = R8Command.builder(handler)
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
+    finishBuild(builder, out, test);
     R8.run(builder.build());
   }
 
@@ -79,7 +110,6 @@
     // Generate R8 processed version without library option.
     String test = "shaking1";
     Path out = temp.getRoot().toPath();
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
 
     // Create a flags file in temp dir requesting dump of the mapping.
@@ -90,10 +120,15 @@
     }
 
     R8Command.Builder builder = R8Command.builder()
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules, printMapping);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
     // Turn off inlining, as we want the mapping that is printed to be stable.
+    finishBuild(builder, out, test);
+    if (backend == Backend.DEX) {
+      builder.addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
 
     Path outputmapping = out.resolve("mapping.txt");
@@ -102,8 +137,15 @@
         Stream.of(new String(Files.readAllBytes(outputmapping), StandardCharsets.UTF_8).split("\n"))
             .filter(line -> !line.startsWith("#"))
             .collect(Collectors.joining("\n"));
-    String refMapping = new String(Files.readAllBytes(
-        Paths.get(EXAMPLES_DIR, "shaking1", "print-mapping.ref")), StandardCharsets.UTF_8);
+    // For the CF backend we treat ConstString/LDC as a (potentially always) throwing instruction,
+    // as opposed to the DEX backend where it's throwing only if its string is ill-formed.
+    // When ConstString is throwing we preserve its position which makes it show up in the
+    // the output Proguard map. That's why the reference CF map is different from the DEX one.
+    String mapping_ref = backend == Backend.CF ? "print-mapping-cf.ref" : "print-mapping.ref";
+    String refMapping =
+        new String(
+            Files.readAllBytes(Paths.get(EXAMPLES_DIR, "shaking1", mapping_ref)),
+            StandardCharsets.UTF_8);
     Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
index eab5197..d753632 100644
--- a/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/defaultmethods/DefaultMethodsTest.java
@@ -7,6 +7,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
@@ -19,18 +20,43 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class DefaultMethodsTest extends TestBase {
+
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public DefaultMethodsTest(Backend backend) {
+    this.backend = backend;
+  }
+
   private void runTest(List<String> additionalKeepRules, Consumer<CodeInspector> inspection)
       throws Exception {
     R8Command.Builder builder = R8Command.builder();
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(InterfaceWithDefaultMethods.class));
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(ClassImplementingInterface.class));
     builder.addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class));
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
-    builder.setMinApiLevel(AndroidApiLevel.O.getLevel());
+    if (backend == Backend.DEX) {
+      builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+      int apiLevel = AndroidApiLevel.O.getLevel();
+      builder.setMinApiLevel(apiLevel);
+      builder.addLibraryFiles(ToolHelper.getAndroidJar(apiLevel));
+    } else {
+      assert backend == Backend.CF;
+      builder.setProgramConsumer(ClassFileConsumer.emptyConsumer());
+      builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     // Always keep main in the test class, so the output never becomes empty.
     builder.addProguardConfiguration(ImmutableList.of(
         "-keep class " + TestClass.class.getCanonicalName() + "{",
diff --git a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
index 8d38aee..b3e6367 100644
--- a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
@@ -6,14 +6,11 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.code.Instruction;
-import com.android.tools.r8.code.InvokeInterface;
-import com.android.tools.r8.code.InvokeVirtual;
-import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.invokesuper.Consumer;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.origin.Origin;
@@ -24,11 +21,32 @@
 import com.android.tools.r8.shaking.proxy.testclasses.TestClass;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.InvokeInstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
+import java.util.function.Predicate;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class ProxiesTest extends TestBase {
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public ProxiesTest(Backend backend) {
+    this.backend = backend;
+  }
 
   private void runTest(List<String> additionalKeepRules, Consumer<CodeInspector> inspection,
       String expectedResult)
@@ -49,52 +67,91 @@
         Origin.unknown()
     );
     builder.addProguardConfiguration(additionalKeepRules, Origin.unknown());
-    builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
+    if (backend == Backend.DEX) {
+      builder
+          .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder
+          .setProgramConsumer(ClassFileConsumer.emptyConsumer())
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableDevirtualization = false);
-    inspection.accept(new CodeInspector(app));
-    assertEquals(expectedResult, runOnArt(app, mainClass));
+    inspection.accept(new CodeInspector(app, o -> o.enableCfFrontend = true));
+    String result = backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass);
+    if (ToolHelper.isWindows()) {
+      result = result.replace(System.lineSeparator(), "\n");
+    }
+    assertEquals(expectedResult, result);
   }
 
-  private int countInstructionInX(CodeInspector inspector, Class<? extends Instruction> invoke) {
+  private int countInstructionInX(CodeInspector inspector, Predicate<InstructionSubject> invoke) {
     MethodSignature signatureForX =
         new MethodSignature("x", "void", ImmutableList.of(BaseInterface.class.getCanonicalName()));
-    DexCode x = inspector.clazz(Main.class).method(signatureForX).getMethod().getCode().asDexCode();
-    return (int) filterInstructionKind(x, invoke).count();
+    MethodSubject method = inspector.clazz(Main.class).method(signatureForX);
+    assert method instanceof FoundMethodSubject;
+    FoundMethodSubject foundMethod = (FoundMethodSubject) method;
+    return (int) Streams.stream(foundMethod.iterateInstructions(invoke)).count();
   }
 
-  private int countInstructionInY(CodeInspector inspector, Class<? extends Instruction> invoke) {
+  private int countInstructionInY(CodeInspector inspector, Predicate<InstructionSubject> invoke) {
     MethodSignature signatureForY =
         new MethodSignature("y", "void", ImmutableList.of(SubInterface.class.getCanonicalName()));
-    DexCode y = inspector.clazz(Main.class).method(signatureForY).getMethod().getCode().asDexCode();
-    return (int) filterInstructionKind(y, invoke)
-        .filter(instruction -> instruction.getMethod().qualifiedName().endsWith("method"))
-        .count();
+    MethodSubject method = inspector.clazz(Main.class).method(signatureForY);
+    assert method instanceof FoundMethodSubject;
+    FoundMethodSubject foundMethod = (FoundMethodSubject) method;
+    return (int)
+        Streams.stream(foundMethod.iterateInstructions(invoke))
+            .filter(
+                instruction -> {
+                  InvokeInstructionSubject invokeInstruction =
+                      (InvokeInstructionSubject) instruction;
+                  return invokeInstruction.invokedMethod().qualifiedName().endsWith("method");
+                })
+            .count();
   }
 
-  private int countInstructionInZ(CodeInspector inspector, Class<? extends Instruction> invoke) {
+  private int countInstructionInZ(CodeInspector inspector, Predicate<InstructionSubject> invoke) {
     MethodSignature signatureForZ =
         new MethodSignature("z", "void", ImmutableList.of(TestClass.class.getCanonicalName()));
-    DexCode z = inspector.clazz(Main.class).method(signatureForZ).getMethod().getCode().asDexCode();
-    return (int) filterInstructionKind(z, invoke)
-        .filter(instruction -> instruction.getMethod().qualifiedName().endsWith("method"))
-        .count();
+    MethodSubject method = inspector.clazz(Main.class).method(signatureForZ);
+    assert method instanceof FoundMethodSubject;
+    FoundMethodSubject foundMethod = (FoundMethodSubject) method;
+    return (int)
+        Streams.stream(foundMethod.iterateInstructions(invoke))
+            .filter(
+                instruction -> {
+                  InvokeInstructionSubject invokeInstruction =
+                      (InvokeInstructionSubject) instruction;
+                  return invokeInstruction.invokedMethod().qualifiedName().endsWith("method");
+                })
+            .count();
   }
 
   private int countInstructionInZSubClass(
-      CodeInspector inspector, Class<? extends Instruction> invoke) {
+      CodeInspector inspector, Predicate<InstructionSubject> invoke) {
     MethodSignature signatureForZ =
         new MethodSignature("z", "void", ImmutableList.of(SubClass.class.getCanonicalName()));
-    DexCode z = inspector.clazz(Main.class).method(signatureForZ).getMethod().getCode().asDexCode();
-    return (int) filterInstructionKind(z, invoke)
-        .filter(instruction -> instruction.getMethod().qualifiedName().endsWith("method"))
-        .count();
+    MethodSubject method = inspector.clazz(Main.class).method(signatureForZ);
+    assert method instanceof FoundMethodSubject;
+    FoundMethodSubject foundMethod = (FoundMethodSubject) method;
+    return (int)
+        Streams.stream(foundMethod.iterateInstructions(invoke))
+            .filter(
+                instruction -> {
+                  InvokeInstructionSubject invokeInstruction =
+                      (InvokeInstructionSubject) instruction;
+                  return invokeInstruction.invokedMethod().qualifiedName().endsWith("method");
+                })
+            .count();
   }
 
   private void noInterfaceKept(CodeInspector inspector) {
     // Indirectly assert that method is inlined into x, y and z.
-    assertEquals(1, countInstructionInX(inspector, InvokeInterface.class));
-    assertEquals(1, countInstructionInY(inspector, InvokeInterface.class));
-    assertEquals(1, countInstructionInZ(inspector, InvokeVirtual.class));
+    assertEquals(1, countInstructionInX(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(1, countInstructionInY(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(1, countInstructionInZ(inspector, InstructionSubject::isInvokeVirtual));
   }
 
   @Test
@@ -106,11 +163,11 @@
 
   private void baseInterfaceKept(CodeInspector inspector) {
     // Indirectly assert that method is not inlined into x.
-    assertEquals(3, countInstructionInX(inspector, InvokeInterface.class));
+    assertEquals(3, countInstructionInX(inspector, InstructionSubject::isInvokeInterface));
     // Indirectly assert that method is inlined into y and z.
-    assertEquals(1, countInstructionInY(inspector, InvokeInterface.class));
-    assertEquals(1, countInstructionInZ(inspector, InvokeVirtual.class));
-    assertEquals(1, countInstructionInZSubClass(inspector, InvokeVirtual.class));
+    assertEquals(1, countInstructionInY(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(1, countInstructionInZ(inspector, InstructionSubject::isInvokeVirtual));
+    assertEquals(1, countInstructionInZSubClass(inspector, InstructionSubject::isInvokeVirtual));
   }
 
   @Test
@@ -126,11 +183,11 @@
 
   private void subInterfaceKept(CodeInspector inspector) {
     // Indirectly assert that method is not inlined into x or y.
-    assertEquals(3, countInstructionInX(inspector, InvokeInterface.class));
-    assertEquals(3, countInstructionInY(inspector, InvokeInterface.class));
+    assertEquals(3, countInstructionInX(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(3, countInstructionInY(inspector, InstructionSubject::isInvokeInterface));
     // Indirectly assert that method is inlined into z.
-    assertEquals(1, countInstructionInZ(inspector, InvokeVirtual.class));
-    assertEquals(1, countInstructionInZSubClass(inspector, InvokeVirtual.class));
+    assertEquals(1, countInstructionInZ(inspector, InstructionSubject::isInvokeVirtual));
+    assertEquals(1, countInstructionInZSubClass(inspector, InstructionSubject::isInvokeVirtual));
   }
 
   @Test
@@ -148,10 +205,10 @@
 
   private void classKept(CodeInspector inspector) {
     // Indirectly assert that method is not inlined into x, y or z.
-    assertEquals(3, countInstructionInX(inspector, InvokeInterface.class));
-    assertEquals(3, countInstructionInY(inspector, InvokeInterface.class));
-    assertEquals(3, countInstructionInZ(inspector, InvokeVirtual.class));
-    assertEquals(3, countInstructionInZSubClass(inspector, InvokeVirtual.class));
+    assertEquals(3, countInstructionInX(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(3, countInstructionInY(inspector, InstructionSubject::isInvokeInterface));
+    assertEquals(3, countInstructionInZ(inspector, InstructionSubject::isInvokeVirtual));
+    assertEquals(3, countInstructionInZSubClass(inspector, InstructionSubject::isInvokeVirtual));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
index 147b1b4..d12f41a 100644
--- a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
@@ -13,6 +13,9 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
@@ -20,17 +23,45 @@
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class ForceInlineTest extends TestBase {
+  private Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public ForceInlineTest(Backend backend) {
+    this.backend = backend;
+  }
 
   private CodeInspector runTest(List<String> proguardConfiguration) throws Exception {
+    ProgramConsumer programConsumer;
+    Path library;
+    if (backend == Backend.DEX) {
+      programConsumer = DexIndexedConsumer.emptyConsumer();
+      library = ToolHelper.getDefaultAndroidJar();
+    } else {
+      assert backend == Backend.CF;
+      programConsumer = ClassFileConsumer.emptyConsumer();
+      library = ToolHelper.getJava8RuntimeJar();
+    }
     R8Command.Builder builder =
-        ToolHelper.prepareR8CommandBuilder(readClasses(Main.class, A.class, B.class, C.class));
+        ToolHelper.prepareR8CommandBuilder(
+                readClasses(Main.class, A.class, B.class, C.class), programConsumer)
+            .addLibraryFiles(library);
     ToolHelper.allowTestProguardOptions(builder);
     builder.addProguardConfiguration(proguardConfiguration, Origin.unknown());
-    return new CodeInspector(ToolHelper.runR8(builder.build()));
+    return new CodeInspector(ToolHelper.runR8(builder.build(), o -> o.enableCfFrontend = true));
   }
 
   @Test