Merge "Mark assertion errors suppressed"
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 4b52a9e..0da9ead 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -95,17 +95,6 @@
     this.debugInfo = debugInfo;
   }
 
-  public boolean hasDebugPositions() {
-    if (debugInfo != null) {
-      for (DexDebugEvent event : debugInfo.events) {
-        if (event instanceof DexDebugEvent.Default) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
   public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
     if (debugInfo == null) {
       return null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index f2cc53f..22a771b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -401,12 +401,6 @@
     }
   }
 
-  public boolean hasDebugPositions() {
-    checkIfObsolete();
-    assert code != null && code.isDexCode();
-    return code.asDexCode().hasDebugPositions();
-  }
-
   public int getClassFileVersion() {
     checkIfObsolete();
     assert classFileVersion >= 0;
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 90a098a..10e960c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -217,6 +217,7 @@
   public final LongMethods longMethods = new LongMethods();
   public final ThrowableMethods throwableMethods = new ThrowableMethods();
   public final ClassMethods classMethods = new ClassMethods();
+  public final EnumMethods enumMethods = new EnumMethods();
   public final PrimitiveTypesBoxedTypeFields primitiveTypesBoxedTypeFields =
       new PrimitiveTypesBoxedTypeFields();
   public final AtomicFieldUpdaterMethods atomicFieldUpdaterMethods =
@@ -408,6 +409,20 @@
     }
   }
 
+  public class EnumMethods {
+
+    public DexMethod valueOf;
+
+    private EnumMethods() {
+      valueOf =
+          createMethod(
+              enumDescriptor,
+              valueOfMethodName,
+              enumDescriptor,
+              new DexString[] {classDescriptor, stringDescriptor});
+    }
+  }
+
   /**
    * All boxed types (Boolean, Byte, ...) have a field named TYPE which contains the Class object
    * for the primitive type.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d3355b4..44a7a8e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -406,7 +406,13 @@
     if (other.getClass() != getClass()) {
       return false;
     }
-    if (!identicalNonValueParts(other)) {
+    // In debug mode or if the instruction can throw we must account for positions, in release mode
+    // we do want to share non-throwing instructions even if their positions differ.
+    if (instructionTypeCanThrow() || allocator.getOptions().debug) {
+      if (!identicalNonValueParts(other)) {
+        return false;
+      }
+    } else if (!identicalNonValueNonPositionParts(other)) {
       return false;
     }
     if (isInvokeDirect() && !asInvokeDirect().sameConstructorReceiverValue(other.asInvoke())) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
index eed1c47..9d36037 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -108,31 +108,12 @@
         // goto A
         //
         // A: ...y // blockWithNonNullInstruction
-        //
+        boolean split = block.hasCatchHandlers();
         BasicBlock blockWithNonNullInstruction =
-            block.hasCatchHandlers() ? iterator.split(code, blockIterator) : block;
-        // Next, add non-null fake IR, e.g.,
-        // ...x
-        // invoke(rcv, ...)
-        // goto A
-        // ...
-        // A: non_null_rcv <- non-null(rcv)
-        // ...y
-        Value nonNullValue = code.createValue(
-            knownToBeNonNullValue.getTypeLattice(),
-            knownToBeNonNullValue.getLocalInfo());
-        nonNullValueCollector.add(nonNullValue);
-        NonNull nonNull = new NonNull(nonNullValue, knownToBeNonNullValue, current);
-        nonNull.setPosition(current.getPosition());
-        if (blockWithNonNullInstruction !=  block) {
-          // If we split, add non-null IR on top of the new split block.
-          blockWithNonNullInstruction.listIterator().add(nonNull);
-        } else {
-          // Otherwise, just add it to the current block at the position of the iterator.
-          iterator.add(nonNull);
-        }
-        // Then, replace all users of the original value that are dominated by either the current
-        // block or the new split-off block. Since NPE can be explicitly caught, nullness should be
+            split ? iterator.split(code, blockIterator) : block;
+
+        // Find all users of the original value that are dominated by either the current block
+        // or the new split-off block. Since NPE can be explicitly caught, nullness should be
         // propagated through dominance.
         Set<Instruction> users = knownToBeNonNullValue.uniqueUsers();
         Set<Instruction> dominatedUsers = Sets.newIdentityHashSet();
@@ -142,14 +123,13 @@
         for (BasicBlock dominatee : dominatorTree.dominatedBlocks(blockWithNonNullInstruction)) {
           dominatedBlocks.add(dominatee);
           InstructionListIterator dominateeIterator = dominatee.listIterator();
-          if (dominatee == blockWithNonNullInstruction) {
-            // In the block with the inserted non null instruction, skip instructions up to and
-            // including the newly inserted instruction.
-            dominateeIterator.nextUntil(instruction -> instruction == nonNull);
+          if (dominatee == blockWithNonNullInstruction && !split) {
+            // In the block where the non null instruction will be inserted, skip instructions up
+            // to and including the insertion point.
+            dominateeIterator.nextUntil(instruction -> instruction == current);
           }
           while (dominateeIterator.hasNext()) {
             Instruction potentialUser = dominateeIterator.next();
-            assert potentialUser != nonNull;
             if (users.contains(potentialUser)) {
               dominatedUsers.add(potentialUser);
             }
@@ -162,8 +142,35 @@
             dominatedPhiUsersWithPositions.put(user, dominatedPredecessorIndexes);
           }
         }
-        knownToBeNonNullValue.replaceSelectiveUsers(
-            nonNullValue, dominatedUsers, dominatedPhiUsersWithPositions);
+
+        // Only insert non-null instruction if it is ever used.
+        if (!dominatedUsers.isEmpty() || !dominatedPhiUsersWithPositions.isEmpty()) {
+          // Add non-null fake IR, e.g.,
+          // ...x
+          // invoke(rcv, ...)
+          // goto A
+          // ...
+          // A: non_null_rcv <- non-null(rcv)
+          // ...y
+          Value nonNullValue =
+              code.createValue(
+                  knownToBeNonNullValue.getTypeLattice(), knownToBeNonNullValue.getLocalInfo());
+          nonNullValueCollector.add(nonNullValue);
+          NonNull nonNull = new NonNull(nonNullValue, knownToBeNonNullValue, current);
+          nonNull.setPosition(current.getPosition());
+          if (blockWithNonNullInstruction != block) {
+            // If we split, add non-null IR on top of the new split block.
+            blockWithNonNullInstruction.listIterator().add(nonNull);
+          } else {
+            // Otherwise, just add it to the current block at the position of the iterator.
+            iterator.add(nonNull);
+          }
+
+          // Replace all users of the original value that are dominated by either the current
+          // block or the new split-off block.
+          knownToBeNonNullValue.replaceSelectiveUsers(
+              nonNullValue, dominatedUsers, dominatedPhiUsersWithPositions);
+        }
       }
 
       // Add non-null on top of the successor block if the current block ends with a null check.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index eb140c4..e6791f7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -253,7 +253,8 @@
             changed = true;
             int otherPredIndex = blockToIndex.get(wrapper);
             BasicBlock otherPred = block.getPredecessors().get(otherPredIndex);
-            assert Objects.equals(pred.getPosition(), otherPred.getPosition());
+            assert !allocator.getOptions().debug
+                || Objects.equals(pred.getPosition(), otherPred.getPosition());
             pred.clearCatchHandlers();
             pred.getInstructions().clear();
             equivalence.clearComputedHash(pred);
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 de42c18..0be9f91 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -68,6 +68,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -253,7 +254,6 @@
 
   private void enqueueRootItems(Map<DexDefinition, ProguardKeepRule> items) {
     items.entrySet().forEach(this::enqueueRootItem);
-    pinnedItems.addAll(items.keySet());
   }
 
   private void enqueueRootItem(Entry<DexDefinition, ProguardKeepRule> root) {
@@ -261,7 +261,10 @@
   }
 
   private void enqueueRootItem(DexDefinition item, ProguardKeepRule rule) {
-    KeepReason reason = KeepReason.dueToKeepRule(rule);
+    enqueueRootItem(item, KeepReason.dueToKeepRule(rule));
+  }
+
+  private void enqueueRootItem(DexDefinition item, KeepReason reason) {
     if (item.isDexClass()) {
       DexClass clazz = item.asDexClass();
       workList.add(Action.markInstantiated(clazz, reason));
@@ -285,6 +288,7 @@
     } else {
       throw new IllegalArgumentException(item.toString());
     }
+    pinnedItems.add(item);
   }
 
   private void enqueueHolderIfDependentNonStaticMember(
@@ -391,6 +395,10 @@
         // Revisit the current method to implicitly add -keep rule for items with reflective access.
         pendingReflectiveUses.add(currentMethod);
       }
+      // See comment in handleJavaLangEnumValueOf.
+      if (method == appInfo.dexItemFactory.enumMethods.valueOf) {
+        pendingReflectiveUses.add(currentMethod);
+      }
       if (!registerItemWithTarget(staticInvokes, method)) {
         return false;
       }
@@ -1150,6 +1158,25 @@
     }
   }
 
+  private DexMethod generatedEnumValuesMethod(DexClass enumClass) {
+    DexType arrayOfEnumClass =
+        appInfo.dexItemFactory.createType(
+            appInfo.dexItemFactory.createString("[" + enumClass.type.toDescriptorString()));
+    DexProto proto = appInfo.dexItemFactory.createProto(arrayOfEnumClass);
+    return appInfo.dexItemFactory.createMethod(
+        enumClass.type, proto, appInfo.dexItemFactory.createString("values"));
+  }
+
+  private void markEnumValuesAsReachable(DexClass clazz, KeepReason reason) {
+    DexEncodedMethod valuesMethod = clazz.lookupMethod(generatedEnumValuesMethod(clazz));
+    if (valuesMethod != null) {
+      // TODO(sgjesse): Does this have to be enqueued as a root item? Right now it is done as the
+      // marking of not renaming is in the root set.
+      enqueueRootItem(valuesMethod, reason);
+      rootSet.noObfuscation.add(valuesMethod);
+    }
+  }
+
   private static void fillWorkList(Deque<DexType> worklist, DexType type) {
     if (type.isInterface()) {
       // We need to check if the method is shadowed by a class that directly implements
@@ -1534,15 +1561,23 @@
     DexType originHolder = method.method.holder;
     Origin origin = appInfo.originFor(originHolder);
     IRCode code = method.buildIR(appInfo, appView.graphLense(), options, origin);
-    code.instructionIterator().forEachRemaining(this::handleReflectiveBehavior);
+    Iterator<Instruction> iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      handleReflectiveBehavior(method, instruction);
+    }
   }
 
-  private void handleReflectiveBehavior(Instruction instruction) {
+  private void handleReflectiveBehavior(DexEncodedMethod method, Instruction instruction) {
     if (!instruction.isInvokeMethod()) {
       return;
     }
     InvokeMethod invoke = instruction.asInvokeMethod();
     DexMethod invokedMethod = invoke.getInvokedMethod();
+    if (invokedMethod == appInfo.dexItemFactory.enumMethods.valueOf) {
+      handleJavaLangEnumValueOf(method, invoke);
+      return;
+    }
     if (!isReflectionMethod(appInfo.dexItemFactory, invokedMethod)) {
       return;
     }
@@ -1577,6 +1612,20 @@
     }
   }
 
+  private void handleJavaLangEnumValueOf(DexEncodedMethod method, InvokeMethod invoke) {
+    // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
+    // access the values() method of the enum class passed as the first argument. The method
+    // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
+    // call this method.
+    if (invoke.inValues().get(0).isConstClass()) {
+      DexClass clazz =
+          appInfo.definitionFor(invoke.inValues().get(0).definition.asConstClass().getValue());
+      if (clazz.accessFlags.isEnum() && clazz.superType == appInfo.dexItemFactory.enumType) {
+        markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(method));
+      }
+    }
+  }
+
   private static class Action {
 
     final Kind kind;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
index 564a81c..e569ad2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullTrackerTest.java
@@ -69,6 +69,8 @@
             || NonNullTracker.throwsOnNullInput(prev)
             || (prev.isIf() && prev.asIf().isZeroTest())
             || !curr.getBlock().getPredecessors().contains(prev.getBlock()));
+        // Make sure non-null is used.
+        assertTrue(curr.outValue().numberOfAllUsers() > 0);
         count++;
       }
     }
@@ -114,7 +116,7 @@
     buildAndTest(NonNullAfterInvoke.class, foo, 1, this::checkInvokeGetsNonNullReceiver);
     MethodSignature bar =
         new MethodSignature("bar", "int", new String[]{"java.lang.String"});
-    buildAndTest(NonNullAfterInvoke.class, bar, 2, this::checkInvokeGetsNullReceiver);
+    buildAndTest(NonNullAfterInvoke.class, bar, 1, this::checkInvokeGetsNullReceiver);
   }
 
   @Test
@@ -176,6 +178,6 @@
     buildAndTest(NonNullAfterNullCheck.class, bar, 1, this::checkInvokeGetsNonNullReceiver);
     MethodSignature baz =
         new MethodSignature("baz", "int", new String[]{"java.lang.String"});
-    buildAndTest(NonNullAfterNullCheck.class, baz, 2, this::checkInvokeGetsNullReceiver);
+    buildAndTest(NonNullAfterNullCheck.class, baz, 1, this::checkInvokeGetsNullReceiver);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinification.java b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
index a8708eb..66cd45e 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinification.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinification.java
@@ -4,17 +4,19 @@
 
 package com.android.tools.r8.naming;
 
-import static org.hamcrest.CoreMatchers.containsString;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -22,6 +24,12 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
 
 @RunWith(Parameterized.class)
 public class EnumMinification extends TestBase {
@@ -37,29 +45,57 @@
     this.backend = backend;
   }
 
+  private AndroidApp buildApp(Class<?> mainClass, byte[] enumClassFile) throws Exception {
+    return ToolHelper.runR8(
+        R8Command.builder()
+            .addClassProgramData(ToolHelper.getClassAsBytes(mainClass), Origin.unknown())
+            .addClassProgramData(enumClassFile, Origin.unknown())
+            .addProguardConfiguration(
+                ImmutableList.of(keepMainProguardConfiguration(mainClass)), Origin.unknown())
+            .setProgramConsumer(emptyConsumer(backend))
+            .build());
+  }
+
+  public void runTest(
+      Class<?> mainClass, byte[] enumClass, String enumTypeName, boolean valueOfKept)
+      throws Exception {
+    AndroidApp output = buildApp(mainClass, enumClass);
+
+    CodeInspector inspector = new CodeInspector(output);
+    ClassSubject clazz = inspector.clazz(enumTypeName);
+    // The class and fields - including field $VALUES and method valueOf - can be renamed. Only
+    // the values() method needs to be
+    assertThat(clazz, isRenamed());
+    assertThat(clazz.field(enumTypeName, "VALUE1"), isRenamed());
+    assertThat(clazz.field(enumTypeName, "VALUE2"), isRenamed());
+    assertThat(clazz.field(enumTypeName + "[]", "$VALUES"), isRenamed());
+    assertThat(
+        clazz.method(enumTypeName, "valueOf", ImmutableList.of("java.lang.String")),
+        valueOfKept ? isRenamed() : not(isPresent()));
+    assertThat(clazz.method(enumTypeName + "[]", "values", ImmutableList.of()), not(isRenamed()));
+
+    assertEquals("VALUE1", runOnVM(output, mainClass, backend));
+  }
+
   @Test
   public void test() throws Exception {
-    AndroidApp output =
-        ToolHelper.runR8(
-            R8Command.builder()
-                .addClassProgramData(ToolHelper.getClassAsBytes(Main.class), Origin.unknown())
-                .addClassProgramData(ToolHelper.getClassAsBytes(Enum.class), Origin.unknown())
-                .addProguardConfiguration(
-                    ImmutableList.of(keepMainProguardConfiguration(Main.class)), Origin.unknown())
-                .setProgramConsumer(emptyConsumer(backend))
-                .build());
+    runTest(Main.class, ToolHelper.getClassAsBytes(Enum.class), Enum.class.getTypeName(), true);
+  }
 
-    // TODO(117299356): valueOf on enum fails for minified enums.
-    ProcessResult result = runOnVMRaw(output, Main.class, backend);
-    assertEquals(1, result.exitCode);
-    assertThat(
-        result.stderr,
-        containsString(
-            backend == Backend.DEX
-                ? ToolHelper.getDexVm().isNewerThan(DexVm.ART_4_4_4_HOST)
-                    ? "java.lang.NoSuchMethodException"
-                    : "java.lang.NullPointerException"
-                : "java.lang.IllegalArgumentException"));
+  @Test
+  public void testAsmDump() throws Exception {
+    runTest(Main.class, EnumDump.dump(true), "com.android.tools.r8.naming.Enum", true);
+  }
+
+  @Test
+  public void testWithoutValuesMethod() throws Exception {
+    // This should not fail even if the values method is not present.
+    buildApp(Main.class, EnumDump.dump(false));
+  }
+
+  @Test
+  public void testJavaLangEnumValueOf() throws Exception {
+    runTest(Main2.class, ToolHelper.getClassAsBytes(Enum.class), Enum.class.getTypeName(), false);
   }
 }
 
@@ -67,7 +103,7 @@
 
   public static void main(String[] args) {
     Enum e = Enum.valueOf("VALUE1");
-    System.out.println(e);
+    System.out.print(e);
   }
 }
 
@@ -75,3 +111,219 @@
   VALUE1,
   VALUE2
 }
+
+class Main2 {
+  public static void main(String[] args) {
+    // Use java.lang.Enum.valueOf instead of com.android.tools.r8.naming.Enum.valueOf.
+    System.out.print(java.lang.Enum.valueOf(Enum.class, "VALUE1"));
+  }
+}
+/* Dump of javac generated code from the following enum class (the one just above):
+ *
+ *  package com.android.tools.r8.naming;
+ *
+ *  enum Enum {
+ *    VALUE1,
+ *    VALUE2
+ *  }
+ *
+ */
+class EnumDump implements Opcodes {
+
+  public static byte[] dump(boolean includeValuesMethod) {
+    ClassWriter classWriter = new ClassWriter(0);
+    FieldVisitor fieldVisitor;
+    MethodVisitor methodVisitor;
+
+    classWriter.visit(
+        V1_8,
+        ACC_FINAL | ACC_SUPER | ACC_ENUM,
+        "com/android/tools/r8/naming/Enum",
+        "Ljava/lang/Enum<Lcom/android/tools/r8/naming/Enum;>;",
+        "java/lang/Enum",
+        null);
+
+    classWriter.visitSource("EnumMinification.java", null);
+
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_PUBLIC | ACC_FINAL | ACC_STATIC | ACC_ENUM,
+              "VALUE1",
+              "Lcom/android/tools/r8/naming/Enum;",
+              null,
+              null);
+      fieldVisitor.visitEnd();
+    }
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_PUBLIC | ACC_FINAL | ACC_STATIC | ACC_ENUM,
+              "VALUE2",
+              "Lcom/android/tools/r8/naming/Enum;",
+              null,
+              null);
+      fieldVisitor.visitEnd();
+    }
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_PRIVATE | ACC_FINAL | ACC_STATIC | ACC_SYNTHETIC,
+              "$VALUES",
+              "[Lcom/android/tools/r8/naming/Enum;",
+              null,
+              null);
+      fieldVisitor.visitEnd();
+    }
+    if (includeValuesMethod) {
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "values",
+                "()[Lcom/android/tools/r8/naming/Enum;",
+                null,
+                null);
+        methodVisitor.visitCode();
+        Label label0 = new Label();
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitLineNumber(72, label0);
+        methodVisitor.visitFieldInsn(
+            GETSTATIC,
+            "com/android/tools/r8/naming/Enum",
+            "$VALUES",
+            "[Lcom/android/tools/r8/naming/Enum;");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "[Lcom/android/tools/r8/naming/Enum;",
+            "clone",
+            "()Ljava/lang/Object;",
+            false);
+        methodVisitor.visitTypeInsn(CHECKCAST, "[Lcom/android/tools/r8/naming/Enum;");
+        methodVisitor.visitInsn(ARETURN);
+        methodVisitor.visitMaxs(1, 0);
+        methodVisitor.visitEnd();
+      }
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC | ACC_STATIC,
+              "valueOf",
+              "(Ljava/lang/String;)Lcom/android/tools/r8/naming/Enum;",
+              null,
+              null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(72, label0);
+      methodVisitor.visitLdcInsn(Type.getType("Lcom/android/tools/r8/naming/Enum;"));
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(
+          INVOKESTATIC,
+          "java/lang/Enum",
+          "valueOf",
+          "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;",
+          false);
+      methodVisitor.visitTypeInsn(CHECKCAST, "com/android/tools/r8/naming/Enum");
+      methodVisitor.visitInsn(ARETURN);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLocalVariable("name", "Ljava/lang/String;", null, label0, label1, 0);
+      methodVisitor.visitMaxs(2, 1);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(ACC_PRIVATE, "<init>", "(Ljava/lang/String;I)V", "()V", null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(72, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitVarInsn(ILOAD, 2);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL, "java/lang/Enum", "<init>", "(Ljava/lang/String;I)V", false);
+      methodVisitor.visitInsn(RETURN);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLocalVariable(
+          "this", "Lcom/android/tools/r8/naming/Enum;", null, label0, label1, 0);
+      methodVisitor.visitMaxs(3, 3);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(73, label0);
+      methodVisitor.visitTypeInsn(NEW, "com/android/tools/r8/naming/Enum");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitLdcInsn("VALUE1");
+      methodVisitor.visitInsn(ICONST_0);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL,
+          "com/android/tools/r8/naming/Enum",
+          "<init>",
+          "(Ljava/lang/String;I)V",
+          false);
+      methodVisitor.visitFieldInsn(
+          PUTSTATIC,
+          "com/android/tools/r8/naming/Enum",
+          "VALUE1",
+          "Lcom/android/tools/r8/naming/Enum;");
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(74, label1);
+      methodVisitor.visitTypeInsn(NEW, "com/android/tools/r8/naming/Enum");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitLdcInsn("VALUE2");
+      methodVisitor.visitInsn(ICONST_1);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL,
+          "com/android/tools/r8/naming/Enum",
+          "<init>",
+          "(Ljava/lang/String;I)V",
+          false);
+      methodVisitor.visitFieldInsn(
+          PUTSTATIC,
+          "com/android/tools/r8/naming/Enum",
+          "VALUE2",
+          "Lcom/android/tools/r8/naming/Enum;");
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(72, label2);
+      methodVisitor.visitInsn(ICONST_2);
+      methodVisitor.visitTypeInsn(ANEWARRAY, "com/android/tools/r8/naming/Enum");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitInsn(ICONST_0);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC,
+          "com/android/tools/r8/naming/Enum",
+          "VALUE1",
+          "Lcom/android/tools/r8/naming/Enum;");
+      methodVisitor.visitInsn(AASTORE);
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitInsn(ICONST_1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC,
+          "com/android/tools/r8/naming/Enum",
+          "VALUE2",
+          "Lcom/android/tools/r8/naming/Enum;");
+      methodVisitor.visitInsn(AASTORE);
+      methodVisitor.visitFieldInsn(
+          PUTSTATIC,
+          "com/android/tools/r8/naming/Enum",
+          "$VALUES",
+          "[Lcom/android/tools/r8/naming/Enum;");
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(4, 0);
+      methodVisitor.visitEnd();
+    }
+    classWriter.visitEnd();
+
+    return classWriter.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java
index 7ef1c3f..e1c61c1 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTest.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
@@ -61,17 +62,47 @@
     return StringUtils.splitLines(ToolHelper.runRetrace(mapFile, stackTraceFile));
   }
 
+  private boolean isDalvik() {
+    return backend == Backend.DEX && ToolHelper.getDexVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST);
+  }
+
   private List<String> extractStackTrace(ProcessResult result) {
     ImmutableList.Builder<String> builder = ImmutableList.builder();
     List<String> stderr = StringUtils.splitLines(result.stderr);
     Iterator<String> iterator = stderr.iterator();
-    while (iterator.hasNext()) {
-      String line = iterator.next();
-      if (line.startsWith("Exception in thread \"main\"")) {
-        break;
-      }
+
+    // A Dalvik stacktrace looks like this:
+    // W(209693) threadid=1: thread exiting with uncaught exception (group=0xf616cb20)  (dalvikvm)
+    // java.lang.NullPointerException
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:133)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:139)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:145)
+    // \tat dalvik.system.NativeStart.main(Native Method)
+    //
+    // An Art 5.1.1 and 6.0.1 stacktrace looks like this:
+    // java.lang.NullPointerException: throw with null exception
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:154)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:160)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:166)
+    //
+    // An Art 7.0.0 and latest stacktrace looks like this:
+    // Exception in thread "main" java.lang.NullPointerException: throw with null exception
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:150)
+    // \tat com.android.tools.r8.naming.retrace.Main.a(:156)
+    // \tat com.android.tools.r8.naming.retrace.Main.main(:162)
+    int last = stderr.size();
+    if (isDalvik()) {
+      // Skip the bottom frame "dalvik.system.NativeStart.main".
+      last--;
     }
-    iterator.forEachRemaining(builder::add);
+    // Take all lines from the bottom starting with "\tat ".
+    int first = last;
+    while (first - 1 >= 0 && stderr.get(first - 1).startsWith("\tat ")) {
+      first--;
+    }
+    for (int i = first; i < last; i++) {
+      builder.add(stderr.get(i));
+    }
     return builder.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTest.java b/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTest.java
new file mode 100644
index 0000000..353952b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTest.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.release;
+
+public class ShareCommonCodeOnDistinctPositionsTest {
+
+  public static void main(String[] args) {
+    int x;
+    int len = args.length;
+    if (len > 42) {
+      x = (len - 2) + len * 2;
+    } else {
+      x = (len - 2) + len * 2;
+    }
+    System.out.println(x);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTestRunner.java b/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTestRunner.java
new file mode 100644
index 0000000..6bfd3d1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/release/ShareCommonCodeOnDistinctPositionsTestRunner.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.release;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.LineNumberTable;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import it.unimi.dsi.fastutil.ints.IntCollection;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ShareCommonCodeOnDistinctPositionsTestRunner extends TestBase {
+
+  private static final Class CLASS = ShareCommonCodeOnDistinctPositionsTest.class;
+
+  @Parameters
+  public static Backend[] parameters() {
+    return Backend.values();
+  }
+
+  private final Backend backend;
+
+  public ShareCommonCodeOnDistinctPositionsTestRunner(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws CompilationFailedException, IOException, ExecutionException {
+    AndroidAppConsumers sink = new AndroidAppConsumers();
+    ToolHelper.runR8(
+        R8Command.builder()
+            .addLibraryFiles(runtimeJar(backend))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(CLASS))
+            .setProgramConsumer(sink.wrapProgramConsumer(emptyConsumer(backend)))
+            .setDisableMinification(true)
+            .setDisableTreeShaking(true)
+            .build(),
+        options -> options.lineNumberOptimization = LineNumberOptimization.OFF);
+    CodeInspector inspector = new CodeInspector(sink.build());
+    MethodSubject method = inspector.clazz(CLASS).mainMethod();
+    // Check that the two shared lines are not in the output (they have no throwing instructions).
+    LineNumberTable lineNumberTable = method.getLineNumberTable();
+    IntCollection lines = lineNumberTable.getLines();
+    assertFalse(lines.contains(12));
+    assertFalse(lines.contains(14));
+    // Check that the two lines have been shared, e.g., there may be only one multiplication left.
+    assertEquals(
+        "Expected only one multiplcation due to instruction sharing.",
+        // TODO(b/117539423): Implement support for sharing optimizations in the CF backend.
+        backend == Backend.DEX ? 1 : 2,
+        Streams.stream(method.iterateInstructions())
+            .filter(InstructionSubject::isMultiplication)
+            .count());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java b/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
index bdd8e1a..51c2c35 100644
--- a/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
+++ b/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
@@ -8,7 +8,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
 import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
 
 import com.android.tools.r8.graph.invokesuper.Consumer;
@@ -103,8 +102,7 @@
       ClassSubject cls = inspector.clazz(clazz);
       assertThat(cls, isPresent());
       assertEquals(1, cls.asFoundClassSubject().allFields().size());
-      // TODD(116079696): This is a hack!
-      cls.forAllFields(field -> assertNotEquals(1, field.getFinalName().length()));
+      cls.forAllFields(field -> assertThat(field, not(isRenamed())));
     }
   }
 
@@ -115,8 +113,7 @@
       assertThat(cls, isPresent());
       assertThat(cls, isRenamed());
       assertEquals(1, cls.asFoundClassSubject().allFields().size());
-      // TODD(116079696): This is a hack!
-      cls.forAllFields(field -> assertEquals(1, field.getFinalName().length()));
+      cls.forAllFields(field -> assertThat(field, isRenamed()));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
index adee8cf..15e8803 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -80,8 +80,8 @@
   }
 
   @Override
-  public boolean hasLineNumberTable() {
-    return false;
+  public LineNumberTable getLineNumberTable() {
+    return null;
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index dedb839..067280a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import com.android.tools.r8.cf.code.CfArithmeticBinop;
 import com.android.tools.r8.cf.code.CfCheckCast;
 import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstString;
@@ -212,4 +213,13 @@
   public boolean isLoad() {
     return instruction instanceof CfLoad;
   }
+
+  @Override
+  public boolean isMultiplication() {
+    if (!(instruction instanceof CfArithmeticBinop)) {
+      return false;
+    }
+    int opcode = ((CfArithmeticBinop) instruction).getAsmOpcode();
+    return Opcodes.IMUL <= opcode && opcode <= Opcodes.DMUL;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index bc31615..28404f0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -39,6 +39,7 @@
 import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
@@ -136,6 +137,23 @@
     return dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptorIgnorePrimitives(string));
   }
 
+  String mapType(Map<String, String> mapping, String typeName) {
+    final String ARRAY_POSTFIX = "[]";
+    int arrayCount = 0;
+    while (typeName.endsWith(ARRAY_POSTFIX)) {
+      arrayCount++;
+      typeName = typeName.substring(0, typeName.length() - 2);
+    }
+    String mappedType = mapping.get(typeName);
+    if (mappedType == null) {
+      return null;
+    }
+    for (int i = 0; i < arrayCount; i++) {
+      mappedType += ARRAY_POSTFIX;
+    }
+    return mappedType;
+  }
+
   static <S, T extends Subject> void forAll(
       S[] items,
       BiFunction<S, FoundClassSubject, ? extends T> constructor,
@@ -248,12 +266,11 @@
   }
 
   String getObfuscatedTypeName(String originalTypeName) {
-    String obfuscatedType = null;
+    String obfuscatedTypeName = null;
     if (mapping != null) {
-      obfuscatedType = originalToObfuscatedMapping.get(originalTypeName);
+      obfuscatedTypeName = mapType(originalToObfuscatedMapping, originalTypeName);
     }
-    obfuscatedType = obfuscatedType == null ? originalTypeName : obfuscatedType;
-    return obfuscatedType;
+    return obfuscatedTypeName != null ? obfuscatedTypeName : originalTypeName;
   }
 
   InstructionSubject createInstructionSubject(Instruction instruction) {
@@ -321,7 +338,7 @@
     public String parsedTypeName(String name) {
       String type = name;
       if (originalToObfuscatedMapping != null) {
-        String original = originalToObfuscatedMapping.inverse().get(name);
+        String original = mapType(originalToObfuscatedMapping.inverse(), name);
         type = original != null ? original : name;
       }
       signature.append(type);
@@ -330,14 +347,17 @@
 
     @Override
     public String parsedInnerTypeName(String enclosingType, String name) {
-      String type;
+      String type = null;
       if (originalToObfuscatedMapping != null) {
         // The enclosingType has already been mapped if a mapping is present.
         String minifiedEnclosing = originalToObfuscatedMapping.get(enclosingType);
-        type = originalToObfuscatedMapping.inverse().get(minifiedEnclosing + "$" + name);
-        if (type != null) {
-          assert type.startsWith(enclosingType + "$");
-          name = type.substring(enclosingType.length() + 1);
+        if (minifiedEnclosing != null) {
+          assert !minifiedEnclosing.contains("[");
+          type = mapType(originalToObfuscatedMapping.inverse(), minifiedEnclosing + "$" + name);
+          if (type != null) {
+            assert type.startsWith(enclosingType + "$");
+            name = type.substring(enclosingType.length() + 1);
+          }
         }
       } else {
         type = enclosingType + "$" + name;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index e1d74c1..ca9ce6a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -46,6 +46,16 @@
 import com.android.tools.r8.code.IputObject;
 import com.android.tools.r8.code.IputShort;
 import com.android.tools.r8.code.IputWide;
+import com.android.tools.r8.code.MulDouble;
+import com.android.tools.r8.code.MulDouble2Addr;
+import com.android.tools.r8.code.MulFloat;
+import com.android.tools.r8.code.MulFloat2Addr;
+import com.android.tools.r8.code.MulInt;
+import com.android.tools.r8.code.MulInt2Addr;
+import com.android.tools.r8.code.MulIntLit16;
+import com.android.tools.r8.code.MulIntLit8;
+import com.android.tools.r8.code.MulLong;
+import com.android.tools.r8.code.MulLong2Addr;
 import com.android.tools.r8.code.NewInstance;
 import com.android.tools.r8.code.Nop;
 import com.android.tools.r8.code.PackedSwitch;
@@ -265,4 +275,18 @@
   public boolean isSparseSwitch() {
     return instruction instanceof SparseSwitch;
   }
+
+  @Override
+  public boolean isMultiplication() {
+    return instruction instanceof MulInt
+        || instruction instanceof MulIntLit8
+        || instruction instanceof MulIntLit16
+        || instruction instanceof MulInt2Addr
+        || instruction instanceof MulFloat
+        || instruction instanceof MulFloat2Addr
+        || instruction instanceof MulLong
+        || instruction instanceof MulLong2Addr
+        || instruction instanceof MulDouble
+        || instruction instanceof MulDouble2Addr;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
index ae42b14..e65a8b0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundFieldSubject.java
@@ -67,9 +67,9 @@
     //
     // whereas the final signature is for X.a is "a a"
     String obfuscatedType = signature.type;
-    String originalType = codeInspector.originalToObfuscatedMapping.inverse().get(obfuscatedType);
+    String originalType =
+        codeInspector.mapType(codeInspector.originalToObfuscatedMapping.inverse(), obfuscatedType);
     String fieldType = originalType != null ? originalType : obfuscatedType;
-
     FieldSignature lookupSignature = new FieldSignature(signature.name, fieldType);
 
     MemberNaming memberNaming = clazz.naming.lookup(lookupSignature);
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index f205594..f4f1660 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -6,20 +6,26 @@
 
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexDebugEvent;
+import com.android.tools.r8.graph.DexDebugInfo;
+import com.android.tools.r8.graph.DexDebugPositionState;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.JarCode;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.Arrays;
 import java.util.Iterator;
-import java.util.ListIterator;
 import java.util.function.Predicate;
-import org.objectweb.asm.tree.AbstractInsnNode;
-import org.objectweb.asm.tree.LineNumberNode;
 
 public class FoundMethodSubject extends MethodSubject {
 
@@ -144,40 +150,6 @@
   }
 
   @Override
-  public boolean hasLineNumberTable() {
-    Code code = getMethod().getCode();
-    if (code.isDexCode()) {
-      DexCode dexCode = code.asDexCode();
-      if (dexCode.getDebugInfo() != null) {
-        for (DexDebugEvent event : dexCode.getDebugInfo().events) {
-          if (event instanceof DexDebugEvent.Default) {
-            return true;
-          }
-        }
-      }
-      return false;
-    }
-    if (code.isCfCode()) {
-      for (CfInstruction insn : code.asCfCode().getInstructions()) {
-        if (insn instanceof CfPosition) {
-          return true;
-        }
-      }
-      return false;
-    }
-    if (code.isJarCode()) {
-      ListIterator<AbstractInsnNode> it = code.asJarCode().getNode().instructions.iterator();
-      while (it.hasNext()) {
-        if (it.next() instanceof LineNumberNode) {
-          return true;
-        }
-      }
-      return false;
-    }
-    throw new Unreachable("Unexpected code type: " + code.getClass().getSimpleName());
-  }
-
-  @Override
   public boolean hasLocalVariableTable() {
     Code code = getMethod().getCode();
     if (code.isDexCode()) {
@@ -207,6 +179,60 @@
   }
 
   @Override
+  public LineNumberTable getLineNumberTable() {
+    Code code = getMethod().getCode();
+    if (code.isDexCode()) {
+      return getDexLineNumberTable(code.asDexCode());
+    }
+    if (code.isCfCode()) {
+      return getCfLineNumberTable(code.asCfCode());
+    }
+    if (code.isJarCode()) {
+      return getJarLineNumberTable(code.asJarCode());
+    }
+    throw new Unreachable("Unexpected code type: " + code.getClass().getSimpleName());
+  }
+
+  private LineNumberTable getJarLineNumberTable(JarCode code) {
+    throw new Unimplemented("No support for inspecting the line number table for JarCode");
+  }
+
+  private LineNumberTable getCfLineNumberTable(CfCode code) {
+    int currentLine = -1;
+    Reference2IntMap<InstructionSubject> lineNumberTable =
+        new Reference2IntOpenHashMap<>(code.getInstructions().size());
+    for (CfInstruction insn : code.getInstructions()) {
+      if (insn instanceof CfPosition) {
+        currentLine = ((CfPosition) insn).getPosition().line;
+      }
+      if (currentLine != -1) {
+        lineNumberTable.put(new CfInstructionSubject(insn), currentLine);
+      }
+    }
+    return currentLine == -1 ? null : new LineNumberTable(lineNumberTable);
+  }
+
+  private LineNumberTable getDexLineNumberTable(DexCode code) {
+    DexDebugInfo debugInfo = code.getDebugInfo();
+    if (debugInfo == null) {
+      return null;
+    }
+    Reference2IntMap<InstructionSubject> lineNumberTable =
+        new Reference2IntOpenHashMap<>(code.instructions.length);
+    DexDebugPositionState state =
+        new DexDebugPositionState(debugInfo.startLine, getMethod().method);
+    Iterator<DexDebugEvent> iterator = Arrays.asList(debugInfo.events).iterator();
+    for (Instruction insn : code.instructions) {
+      int offset = insn.getOffset();
+      while (state.getCurrentPc() < offset && iterator.hasNext()) {
+        iterator.next().accept(state);
+      }
+      lineNumberTable.put(new DexInstructionSubject(insn), state.getCurrentLine());
+    }
+    return new LineNumberTable(lineNumberTable);
+  }
+
+  @Override
   public String toString() {
     return dexMethod.toSourceString();
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index fb0e0d3..6890684 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -65,4 +65,6 @@
   boolean isPackedSwitch();
 
   boolean isSparseSwitch();
+
+  boolean isMultiplication();
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/LineNumberTable.java b/src/test/java/com/android/tools/r8/utils/codeinspector/LineNumberTable.java
new file mode 100644
index 0000000..7c9c30f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/LineNumberTable.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.codeinspector;
+
+import it.unimi.dsi.fastutil.ints.IntCollection;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+
+public class LineNumberTable {
+  private final Reference2IntMap<InstructionSubject> lineNumberTable;
+
+  public LineNumberTable(Reference2IntMap<InstructionSubject> lineNumberTable) {
+    this.lineNumberTable = lineNumberTable;
+  }
+
+  public IntCollection getLines() {
+    return lineNumberTable.values();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index 55030ca..1e21552 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -33,7 +33,11 @@
     return null;
   }
 
-  public abstract boolean hasLineNumberTable();
+  public boolean hasLineNumberTable() {
+    return getLineNumberTable() != null;
+  }
+
+  public abstract LineNumberTable getLineNumberTable();
 
   public abstract boolean hasLocalVariableTable();
 }