Merge "Collect wildcards while parsing Proguard configuration contents."
diff --git a/.gitignore b/.gitignore
index 41e22c4..3bb46e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 build/
 buildSrc/out/
 .idea/
-r8.iml
 .gradle/
 android-data*/
 tests/2016-12-19/art.tar.gz
@@ -68,6 +67,8 @@
 third_party/core-lambda-stubs.tar.gz
 third_party/openjdk/openjdk-rt-1.8
 third_party/openjdk/openjdk-rt-1.8.tar.gz
+third_party/r8
+third_party/r8.tar.gz
 src/test/jack/ub-jack
 gradle-app.setting
 gradlew
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6821c4e..df3d81b 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -29,7 +29,6 @@
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.naming.SeedMapper;
 import com.android.tools.r8.naming.SourceFileRewriter;
-import com.android.tools.r8.optimize.BridgeMethodAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
 import com.android.tools.r8.origin.CommandLineOrigin;
@@ -359,11 +358,15 @@
         }
         application = application.asDirect().rewrittenWithLense(graphLense);
         appInfo = appInfo.withLiveness().rewrittenWithLense(application.asDirect(), graphLense);
-        // Collect switch maps and ordinals maps.
-        appInfo = new SwitchMapCollector(appInfo.withLiveness(), options).run();
-        appInfo = new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
+        // TODO(mathiasr): Remove this check when CF->IR construction is complete.
+        if (!options.skipIR) {
+          // Collect switch maps and ordinals maps.
+          appInfo = new SwitchMapCollector(appInfo.withLiveness(), options).run();
+          appInfo = new EnumOrdinalMapCollector(appInfo.withLiveness(), options).run();
+        }
 
-        graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withLiveness()).run();
+        // TODO(b/79143143): re-enable once fixed.
+        // graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withLiveness()).run();
       }
 
       timing.begin("Create IR");
diff --git a/src/main/java/com/android/tools/r8/ResourceShrinker.java b/src/main/java/com/android/tools/r8/ResourceShrinker.java
index d2969d6..7fee6fe 100644
--- a/src/main/java/com/android/tools/r8/ResourceShrinker.java
+++ b/src/main/java/com/android/tools/r8/ResourceShrinker.java
@@ -153,8 +153,9 @@
       }
 
       for (DexEncodedField field : classDef.staticFields()) {
-        if (field.staticValue != null) {
-          processFieldValue(field.staticValue);
+        DexValue staticValue = field.getStaticValue();
+        if (staticValue != null) {
+          processFieldValue(staticValue);
         }
       }
 
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 2fe8952..b116e65 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "v1.2.11-dev";
+  public static final String LABEL = "v1.2.14-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/cf/CfPrinter.java b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
index 181ebac..300335a 100644
--- a/src/main/java/com/android/tools/r8/cf/CfPrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfPrinter.java
@@ -16,12 +16,11 @@
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
 import com.android.tools.r8.cf.code.CfFrame;
-import com.android.tools.r8.cf.code.CfFrame.Uninitialized;
-import com.android.tools.r8.cf.code.CfFrame.UninitializedNew;
-import com.android.tools.r8.cf.code.CfFrame.UninitializedThis;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.cf.code.CfGoto;
 import com.android.tools.r8.cf.code.CfIf;
 import com.android.tools.r8.cf.code.CfIfCmp;
+import com.android.tools.r8.cf.code.CfIinc;
 import com.android.tools.r8.cf.code.CfInstanceOf;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
@@ -33,10 +32,10 @@
 import com.android.tools.r8.cf.code.CfNew;
 import com.android.tools.r8.cf.code.CfNewArray;
 import com.android.tools.r8.cf.code.CfNop;
-import com.android.tools.r8.cf.code.CfPop;
 import com.android.tools.r8.cf.code.CfPosition;
 import com.android.tools.r8.cf.code.CfReturn;
 import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfStore;
 import com.android.tools.r8.cf.code.CfSwitch;
 import com.android.tools.r8.cf.code.CfSwitch.Kind;
@@ -57,8 +56,6 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.StringUtils.BraceType;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.IntList;
 import java.util.HashMap;
@@ -152,6 +149,40 @@
     print("nop");
   }
 
+  public void print(CfStackInstruction instruction) {
+    switch (instruction.getOpcode()) {
+      case Pop:
+        print("pop");
+        return;
+      case Pop2:
+        print("pop2");
+        return;
+      case Dup:
+        print("dup");
+        return;
+      case DupX1:
+        print("dup_x1");
+        return;
+      case DupX2:
+        print("dup_x2");
+        return;
+      case Dup2:
+        print("dup2");
+        return;
+      case Dup2X1:
+        print("dup2_x1");
+        return;
+      case Dup2X2:
+        print("dup2_x2");
+        return;
+      case Swap:
+        print("swap");
+        return;
+      default:
+        throw new Unreachable("Invalid instruction for CfStackInstruction");
+    }
+  }
+
   public void print(CfThrow insn) {
     print("athrow");
   }
@@ -207,10 +238,6 @@
     print(opcodeName(unop.getOpcode()));
   }
 
-  public void print(CfPop pop) {
-    print("pop");
-  }
-
   public void print(CfConstString constString) {
     indent();
     builder.append("ldc ").append(constString.getString());
@@ -241,26 +268,35 @@
 
   public void print(CfFrame frame) {
     StringBuilder builder = new StringBuilder("frame: [");
-    String separator = "";
-    for (Entry<DexType> entry : frame.getLocals().int2ReferenceEntrySet()) {
-      builder.append(separator).append(entry.getIntKey()).append(':');
-      Uninitialized allocator = frame.getAllocators().get(entry.getIntKey());
-      if (allocator == null) {
-        builder.append(entry.getValue());
-      } else if (allocator instanceof UninitializedThis) {
-        builder.append("uninitialized this");
-      } else {
-        builder
-            .append("uninitialized ")
-            .append(getLabel(((UninitializedNew) allocator).getLabel()));
+    {
+      String separator = "";
+      for (Entry<FrameType> entry : frame.getLocals().int2ReferenceEntrySet()) {
+        builder.append(separator).append(entry.getIntKey()).append(':');
+        print(entry.getValue(), builder);
+        separator = ", ";
       }
-      separator = ", ";
     }
-    builder.append("] ");
-    StringUtils.append(builder, frame.getStack(), ", ", BraceType.SQUARE);
+    builder.append("] [");
+    {
+      String separator = "";
+      for (FrameType element : frame.getStack()) {
+        builder.append(separator);
+        print(element, builder);
+        separator = ", ";
+      }
+    }
+    builder.append(']');
     comment(builder.toString());
   }
 
+  private void print(FrameType type, StringBuilder builder) {
+    if (type.isUninitializedNew()) {
+      builder.append("uninitialized ").append(getLabel(type.getUninitializedLabel()));
+    } else {
+      builder.append(type.toString());
+    }
+  }
+
   public void print(CfInstanceOf insn) {
     indent();
     builder.append("instanceof ");
@@ -376,14 +412,16 @@
 
   public void print(CfSwitch cfSwitch) {
     indent();
-    builder.append(cfSwitch.getKind() == Kind.LOOKUP ? "lookup" : "table").append("switch");
+    Kind kind = cfSwitch.getKind();
+    builder.append(kind == Kind.LOOKUP ? "lookup" : "table").append("switch");
     IntList keys = cfSwitch.getKeys();
     List<CfLabel> targets = cfSwitch.getTargets();
-    for (int i = 0; i < keys.size(); i++) {
+    for (int i = 0; i < targets.size(); i++) {
       indent();
+      int key = kind == Kind.LOOKUP ? keys.getInt(i) : (keys.getInt(0) + i);
       builder
           .append("  ")
-          .append(keys.getInt(i))
+          .append(key)
           .append(": ")
           .append(getLabel(targets.get(i)));
     }
@@ -401,6 +439,15 @@
     printPrefixed(store.getType(), "store", store.getLocalIndex());
   }
 
+  public void print(CfIinc instruction) {
+    indent();
+    builder
+        .append("iinc ")
+        .append(instruction.getLocalIndex())
+        .append(' ')
+        .append(instruction.getIncrement());
+  }
+
   private void printPrefixed(ValueType type, String instruction, int local) {
     indent();
     builder.append(typePrefix(type)).append(instruction).append(' ').append(local);
diff --git a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
index ae43001..b4a6009 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.regalloc.LiveIntervals.NO_REGISTER;
 
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -87,11 +86,6 @@
   }
 
   @Override
-  public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
-    throw new Unreachable();
-  }
-
-  @Override
   public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
     return getRegisterForValue(value);
   }
@@ -144,6 +138,7 @@
 
     while (!unhandled.isEmpty()) {
       LiveIntervals unhandledInterval = unhandled.poll();
+      assert !unhandledInterval.getValue().isArgument();
       int start = unhandledInterval.getStart();
       {
         // Check for active intervals that expired or became inactive.
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
index 07fd664..f3aa7f3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
@@ -56,6 +56,10 @@
           int value = getIntValue();
           if (-1 <= value && value <= 5) {
             visitor.visitInsn(Opcodes.ICONST_0 + value);
+          } else if (Byte.MIN_VALUE <= value && value <= Byte.MAX_VALUE) {
+            visitor.visitIntInsn(Opcodes.BIPUSH, value);
+          } else if (Short.MIN_VALUE <= value && value <= Short.MAX_VALUE) {
+            visitor.visitIntInsn(Opcodes.SIPUSH, value);
           } else {
             visitor.visitLdcInsn(value);
           }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index b5e759c..d015de1 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -12,61 +12,155 @@
 import com.android.tools.r8.naming.NamingLens;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.util.List;
+import java.util.Objects;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
 
 public class CfFrame extends CfInstruction {
 
-  public abstract static class Uninitialized {
-    abstract Object getAsmLabel();
+  public abstract static class FrameType {
+
+    public static FrameType initialized(DexType type) {
+      return new InitializedType(type);
+    }
+
+    public static FrameType uninitializedNew(CfLabel label) {
+      return new UninitializedNew(label);
+    }
+
+    public static FrameType uninitializedThis() {
+      return new UninitializedThis();
+    }
+
+    public static FrameType top() {
+      return Top.SINGLETON;
+    }
+
+    abstract Object getTypeOpcode(NamingLens lens);
+
+    public boolean isWide() {
+      return false;
+    }
+
+    public boolean isUninitializedNew() {
+      return false;
+    }
+
+    public CfLabel getUninitializedLabel() {
+      return null;
+    }
+
+    private FrameType() {}
   }
 
-  public static class UninitializedNew extends Uninitialized {
+  private static class InitializedType extends FrameType {
+
+    private final DexType type;
+
+    private InitializedType(DexType type) {
+      assert type != null;
+      this.type = type;
+    }
+
+    @Override
+    public String toString() {
+      return type.toString();
+    }
+
+    @Override
+    Object getTypeOpcode(NamingLens lens) {
+      if (type == DexItemFactory.nullValueType) {
+        return Opcodes.NULL;
+      }
+      switch (type.toShorty()) {
+        case 'L':
+          return lens.lookupInternalName(type);
+        case 'I':
+          return Opcodes.INTEGER;
+        case 'F':
+          return Opcodes.FLOAT;
+        case 'J':
+          return Opcodes.LONG;
+        case 'D':
+          return Opcodes.DOUBLE;
+        default:
+          throw new Unreachable("Unexpected value type: " + type);
+      }
+    }
+
+    @Override
+    public boolean isWide() {
+      return type.isPrimitiveType() && (type.toShorty() == 'J' || type.toShorty() == 'D');
+    }
+  }
+
+  private static class Top extends FrameType {
+
+    private static final Top SINGLETON = new Top();
+
+    @Override
+    public String toString() {
+      return "top";
+    }
+
+    @Override
+    Object getTypeOpcode(NamingLens lens) {
+      return Opcodes.TOP;
+    }
+  }
+
+  private static class UninitializedNew extends FrameType {
     private final CfLabel label;
 
-    public UninitializedNew(CfLabel label) {
+    private UninitializedNew(CfLabel label) {
       this.label = label;
     }
 
     @Override
-    Object getAsmLabel() {
+    public String toString() {
+      return "uninitialized new";
+    }
+
+    @Override
+    Object getTypeOpcode(NamingLens lens) {
       return label.getLabel();
     }
 
-    public CfLabel getLabel() {
+    @Override
+    public CfLabel getUninitializedLabel() {
       return label;
     }
   }
 
-  public static class UninitializedThis extends Uninitialized {
+  private static class UninitializedThis extends FrameType {
+    private UninitializedThis() {}
+
     @Override
-    Object getAsmLabel() {
+    Object getTypeOpcode(NamingLens lens) {
       return Opcodes.UNINITIALIZED_THIS;
     }
+
+    @Override
+    public String toString() {
+      return "uninitialized this";
+    }
   }
 
-  private final Int2ReferenceSortedMap<DexType> locals;
-  private final Int2ReferenceSortedMap<Uninitialized> allocators;
-  private final List<DexType> stack;
+  private final Int2ReferenceSortedMap<FrameType> locals;
+  private final List<FrameType> stack;
 
-  public CfFrame(
-      Int2ReferenceSortedMap<DexType> locals,
-      Int2ReferenceSortedMap<Uninitialized> allocators,
-      List<DexType> stack) {
+  public CfFrame(Int2ReferenceSortedMap<FrameType> locals, List<FrameType> stack) {
+    assert locals.values().stream().allMatch(Objects::nonNull);
+    assert stack.stream().allMatch(Objects::nonNull);
     this.locals = locals;
-    this.allocators = allocators;
     this.stack = stack;
   }
 
-  public Int2ReferenceSortedMap<DexType> getLocals() {
+  public Int2ReferenceSortedMap<FrameType> getLocals() {
     return locals;
   }
 
-  public Int2ReferenceSortedMap<Uninitialized> getAllocators() {
-    return allocators;
-  }
-
-  public List<DexType> getStack() {
+  public List<FrameType> getStack() {
     return stack;
   }
 
@@ -79,10 +173,6 @@
     visitor.visitFrame(F_NEW, localsCount, localsTypes, stackCount, stackTypes);
   }
 
-  private boolean isWide(DexType type) {
-    return type.isPrimitiveType() && (type.toShorty() == 'J' || type.toShorty() == 'D');
-  }
-
   private int computeStackCount() {
     return stack.size();
   }
@@ -94,7 +184,7 @@
     }
     Object[] stackTypes = new Object[stackCount];
     for (int i = 0; i < stackCount; i++) {
-      stackTypes[i] = getType(stack.get(i), lens);
+      stackTypes[i] = stack.get(i).getTypeOpcode(lens);
     }
     return stackTypes;
   }
@@ -108,8 +198,8 @@
     int localsCount = 0;
     for (int i = 0; i <= maxRegister; i++) {
       localsCount++;
-      DexType type = locals.get(i);
-      if (type != null && isWide(type)) {
+      FrameType type = locals.get(i);
+      if (type != null && type.isWide()) {
         i++;
       }
     }
@@ -124,40 +214,15 @@
     Object[] localsTypes = new Object[localsCount];
     int localIndex = 0;
     for (int i = 0; i <= maxRegister; i++) {
-      DexType type = locals.get(i);
-      Uninitialized allocator = allocators.get(i);
-      Object typeOpcode = allocator == null ? getType(type, lens) : allocator.getAsmLabel();
-      localsTypes[localIndex++] = typeOpcode;
-      if (type != null && isWide(type)) {
+      FrameType type = locals.get(i);
+      localsTypes[localIndex++] = type == null ? Opcodes.TOP : type.getTypeOpcode(lens);
+      if (type != null && type.isWide()) {
         i++;
       }
     }
     return localsTypes;
   }
 
-  private Object getType(DexType type, NamingLens lens) {
-    if (type == null) {
-      return Opcodes.TOP;
-    }
-    if (type == DexItemFactory.nullValueType) {
-      return Opcodes.NULL;
-    }
-    switch (type.toShorty()) {
-      case 'L':
-        return lens.lookupInternalName(type);
-      case 'I':
-        return Opcodes.INTEGER;
-      case 'F':
-        return Opcodes.FLOAT;
-      case 'J':
-        return Opcodes.LONG;
-      case 'D':
-        return Opcodes.DOUBLE;
-      default:
-        throw new Unreachable("Unexpected value type: " + type);
-    }
-  }
-
   @Override
   public String toString() {
     return getClass().getSimpleName();
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
new file mode 100644
index 0000000..6db7250
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
@@ -0,0 +1,37 @@
+// 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.cf.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.naming.NamingLens;
+import org.objectweb.asm.MethodVisitor;
+
+public class CfIinc extends CfInstruction {
+
+  private final int var;
+  private final int increment;
+
+  public CfIinc(int var, int increment) {
+    this.var = var;
+    this.increment = increment;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor, NamingLens lens) {
+    visitor.visitIincInsn(var, increment);
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+
+  public int getLocalIndex() {
+    return var;
+  }
+
+  public int getIncrement() {
+    return increment;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfPop.java b/src/main/java/com/android/tools/r8/cf/code/CfPop.java
deleted file mode 100644
index 9f485d2..0000000
--- a/src/main/java/com/android/tools/r8/cf/code/CfPop.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.cf.code;
-
-import com.android.tools.r8.cf.CfPrinter;
-import com.android.tools.r8.ir.code.ValueType;
-import com.android.tools.r8.naming.NamingLens;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
-
-public class CfPop extends CfInstruction {
-
-  private final ValueType type;
-
-  public CfPop(ValueType type) {
-    this.type = type;
-  }
-
-  @Override
-  public void write(MethodVisitor visitor, NamingLens lens) {
-    visitor.visitInsn(type.isWide() ? Opcodes.POP2 : Opcodes.POP);
-  }
-
-  @Override
-  public void print(CfPrinter printer) {
-    printer.print(this);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
new file mode 100644
index 0000000..efda3f6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
@@ -0,0 +1,81 @@
+// 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.cf.code;
+
+import com.android.tools.r8.cf.CfPrinter;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.naming.NamingLens;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class CfStackInstruction extends CfInstruction {
+
+  public enum Opcode {
+    Pop(Opcodes.POP),
+    Pop2(Opcodes.POP2),
+    Dup(Opcodes.DUP),
+    DupX1(Opcodes.DUP_X1),
+    DupX2(Opcodes.DUP_X2),
+    Dup2(Opcodes.DUP2),
+    Dup2X1(Opcodes.DUP2_X1),
+    Dup2X2(Opcodes.DUP2_X2),
+    Swap(Opcodes.SWAP);
+
+    private final int opcode;
+
+    Opcode(int opcode) {
+      this.opcode = opcode;
+    }
+  }
+
+  private final Opcode opcode;
+
+  public static CfStackInstruction fromAsm(int opcode) {
+    switch (opcode) {
+      case Opcodes.POP:
+        return new CfStackInstruction(Opcode.Pop);
+      case Opcodes.POP2:
+        return new CfStackInstruction(Opcode.Pop2);
+      case Opcodes.DUP:
+        return new CfStackInstruction(Opcode.Dup);
+      case Opcodes.DUP_X1:
+        return new CfStackInstruction(Opcode.DupX1);
+      case Opcodes.DUP_X2:
+        return new CfStackInstruction(Opcode.DupX2);
+      case Opcodes.DUP2:
+        return new CfStackInstruction(Opcode.Dup2);
+      case Opcodes.DUP2_X1:
+        return new CfStackInstruction(Opcode.Dup2X1);
+      case Opcodes.DUP2_X2:
+        return new CfStackInstruction(Opcode.Dup2X2);
+      case Opcodes.SWAP:
+        return new CfStackInstruction(Opcode.Swap);
+      default:
+        throw new Unreachable("Invalid opcode for CfStackInstruction");
+    }
+  }
+
+  public static CfStackInstruction popType(ValueType type) {
+    return new CfStackInstruction(type.isWide() ? Opcode.Pop2 : Opcode.Pop);
+  }
+
+  public CfStackInstruction(Opcode opcode) {
+    this.opcode = opcode;
+  }
+
+  @Override
+  public void write(MethodVisitor visitor, NamingLens lens) {
+    visitor.visitInsn(opcode.opcode);
+  }
+
+  @Override
+  public void print(CfPrinter printer) {
+    printer.print(this);
+  }
+
+  public Opcode getOpcode() {
+    return opcode;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
index f27064c..1dc72d4 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
@@ -20,12 +20,13 @@
   private final int[] keys;
   private final List<CfLabel> targets;
 
-  public CfSwitch(CfLabel defaultTarget, int[] keys, List<CfLabel> targets) {
-    // TODO(zerny): Support emitting table switches.
-    this.kind = Kind.LOOKUP;
+  public CfSwitch(Kind kind, CfLabel defaultTarget, int[] keys, List<CfLabel> targets) {
+    this.kind = kind;
     this.defaultTarget = defaultTarget;
     this.keys = keys;
     this.targets = targets;
+    assert kind != Kind.LOOKUP || keys.length == targets.size();
+    assert kind != Kind.TABLE || keys.length == 1;
   }
 
   public Kind getKind() {
@@ -50,7 +51,16 @@
     for (int i = 0; i < targets.size(); i++) {
       labels[i] = targets.get(i).getLabel();
     }
-    visitor.visitLookupSwitchInsn(defaultTarget.getLabel(), keys, labels);
+    switch (kind) {
+      case LOOKUP:
+        visitor.visitLookupSwitchInsn(defaultTarget.getLabel(), keys, labels);
+        break;
+      case TABLE: {
+        int min = keys[0];
+        int max = min + labels.length - 1;
+        visitor.visitTableSwitchInsn(min, max, defaultTarget.getLabel(), labels);
+      }
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
index a0d1f6a..6dceda0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
@@ -16,17 +16,23 @@
   public final List<DexType> guards;
   public final List<CfLabel> targets;
 
-  public CfTryCatch(
+  public CfTryCatch(CfLabel start, CfLabel end, List<DexType> guards, List<CfLabel> targets) {
+    this.start = start;
+    this.end = end;
+    this.guards = guards;
+    this.targets = targets;
+  }
+
+  public static CfTryCatch fromBuilder(
       CfLabel start,
       CfLabel end,
       CatchHandlers<BasicBlock> handlers,
       CfBuilder builder) {
-    this.start = start;
-    this.end = end;
-    guards = handlers.getGuards();
-    targets = new ArrayList<>(handlers.getAllTargets().size());
+    List<DexType> guards = handlers.getGuards();
+    ArrayList<CfLabel> targets = new ArrayList<>(handlers.getAllTargets().size());
     for (BasicBlock block : handlers.getAllTargets()) {
       targets.add(builder.getLabel(block));
     }
+    return new CfTryCatch(start, end, guards, targets);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 958deb4..4524505 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -576,8 +576,6 @@
       if (accessFlags.isStatic()) {
         if (staticValues != null && i < staticValues.length) {
           staticValue = staticValues[i];
-        } else {
-          staticValue = DexValue.defaultForType(field.type, dexItemFactory);
         }
       }
       fields[i] = new DexEncodedField(field, accessFlags, fieldAnnotations, staticValue);
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 61cd5a7..ab1eca5 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -45,6 +45,11 @@
       this.start = start;
     }
 
+    public LocalVariableInfo(int index, DebugLocalInfo local, CfLabel start, CfLabel end) {
+      this(index, local, start);
+      setEnd(end);
+    }
+
     public void setEnd(CfLabel end) {
       assert this.end == null;
       assert end != null;
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java
index 336eac5..e6b4a2c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSetRefList.java
@@ -12,6 +12,7 @@
   private static final DexAnnotationSetRefList theEmptyTypeList = new DexAnnotationSetRefList();
 
   public final DexAnnotationSet[] values;
+  private final int missingParameterAnnotations;
 
   public static DexAnnotationSetRefList empty() {
     return theEmptyTypeList;
@@ -19,11 +20,17 @@
 
   private DexAnnotationSetRefList() {
     this.values = new DexAnnotationSet[0];
+    this.missingParameterAnnotations = 0;
   }
 
   public DexAnnotationSetRefList(DexAnnotationSet[] values) {
+    this(values, 0);
+  }
+
+  public DexAnnotationSetRefList(DexAnnotationSet[] values, int missingParameterAnnotations) {
     assert values != null && values.length > 0;
     this.values = values;
+    this.missingParameterAnnotations = missingParameterAnnotations;
   }
 
   @Override
@@ -57,4 +64,8 @@
   public boolean isEmpty() {
     return values.length == 0;
   }
+
+  public int getMissingParameterAnnotations() {
+    return missingParameterAnnotations;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 20949c3..b5a3fc2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableSet;
@@ -66,7 +67,9 @@
   // code, but this non-determinism exists even with the same order of classes since we
   // may process classes concurrently and fail-fast on the first error.
   private <T> boolean reorderClasses(List<T> classes) {
-    Collections.shuffle(classes);
+    if (!InternalOptions.DETERMINISTIC_DEBUGGING) {
+      Collections.shuffle(classes);
+    }
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index e9b9255..7b09cab 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -369,7 +369,7 @@
 
   public boolean defaultValuesForStaticFieldsMayTriggerAllocation() {
     return Arrays.stream(staticFields())
-        .anyMatch(field -> !field.staticValue.mayTriggerAllocation());
+        .anyMatch(field -> !field.getStaticValue().mayTriggerAllocation());
   }
 
   public List<InnerClassAttribute> getInnerClasses() {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index bdfadfc..a28dc82 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -15,14 +15,13 @@
   public final DexField field;
   public final FieldAccessFlags accessFlags;
   public DexAnnotationSet annotations;
-  public DexValue staticValue;
+  private DexValue staticValue;
 
   public DexEncodedField(
       DexField field,
       FieldAccessFlags accessFlags,
       DexAnnotationSet annotations,
       DexValue staticValue) {
-    assert !accessFlags.isStatic() || staticValue != null;
     this.field = field;
     this.accessFlags = accessFlags;
     this.annotations = annotations;
@@ -33,8 +32,8 @@
   public void collectIndexedItems(IndexedItemCollection indexedItems) {
     field.collectIndexedItems(indexedItems);
     annotations.collectIndexedItems(indexedItems);
-    if (staticValue != null) {
-      staticValue.collectIndexedItems(indexedItems);
+    if (accessFlags.isStatic()) {
+      getStaticValue().collectIndexedItems(indexedItems);
     }
   }
 
@@ -67,6 +66,22 @@
     return !annotations.isEmpty();
   }
 
+  public boolean hasExplicitStaticValue() {
+    assert accessFlags.isStatic();
+    return staticValue != null;
+  }
+
+  public void setStaticValue(DexValue staticValue) {
+    assert accessFlags.isStatic();
+    assert staticValue != null;
+    this.staticValue = staticValue;
+  }
+
+  public DexValue getStaticValue() {
+    assert accessFlags.isStatic();
+    return staticValue == null ? DexValue.defaultForType(field.type) : staticValue;
+  }
+
   // Returns a const instructions if this field is a compile time final const.
   public Instruction valueAsConstInstruction(AppInfo appInfo, Value dest) {
     // The only way to figure out whether the DexValue contains the final value
@@ -74,7 +89,7 @@
     if (accessFlags.isStatic() && accessFlags.isPublic() && accessFlags.isFinal()) {
       DexClass clazz = appInfo.definitionFor(field.getHolder());
       assert clazz != null : "Class for the field must be present";
-      return staticValue.asConstInstruction(clazz.hasClassInitializer(), dest);
+      return getStaticValue().asConstInstruction(clazz.hasClassInitializer(), dest);
     }
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
index 9644558..0fac5e8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
@@ -49,7 +49,7 @@
     // Set all static field values to unknown. We don't want to use the value from the library
     // at compile time, as it can be different at runtime.
     for (DexEncodedField staticField : staticFields) {
-      staticField.staticValue = DexValue.UNKNOWN;
+      staticField.setStaticValue(DexValue.UNKNOWN);
     }
     assert kind == Kind.CF : "Invalid kind " + kind + " for library-path class " + type;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 90db1d2..1473191 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -275,9 +275,10 @@
         List<DexValue> values = new ArrayList<>(fields.length);
         for (int i = 0; i < fields.length; i++) {
           DexEncodedField field = fields[i];
-          assert field.staticValue != null;
-          values.add(field.staticValue);
-          if (!field.staticValue.isDefault(field.field.type, factory)) {
+          DexValue staticValue = field.getStaticValue();
+          assert staticValue != null;
+          values.add(staticValue);
+          if (!staticValue.isDefault(field.field.type)) {
             length = i + 1;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 570aa6f..3d0a177 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -92,7 +92,8 @@
   }
 
   public boolean isInterface() {
-    assert isClassType() && hierarchyLevel != UNKNOWN_LEVEL;
+    assert hierarchyLevel != UNKNOWN_LEVEL : "Program class missing: " + this;
+    assert isClassType();
     return hierarchyLevel == INTERFACE_LEVEL;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index f1a0f4d..342ea2a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -96,35 +96,29 @@
   @Override
   public abstract String toString();
 
-  public static DexValue defaultForType(DexType type, DexItemFactory factory) {
-    if (type == factory.booleanType) {
-      return DexValueBoolean.DEFAULT;
+  public static DexValue defaultForType(DexType type) {
+    switch (type.toShorty()) {
+      case 'Z':
+        return DexValueBoolean.DEFAULT;
+      case 'B':
+        return DexValueByte.DEFAULT;
+      case 'C':
+        return DexValueChar.DEFAULT;
+      case 'S':
+        return DexValueShort.DEFAULT;
+      case 'I':
+        return DexValueInt.DEFAULT;
+      case 'J':
+        return DexValueLong.DEFAULT;
+      case 'F':
+        return DexValueFloat.DEFAULT;
+      case 'D':
+        return DexValueDouble.DEFAULT;
+      case 'L':
+        return DexValueNull.NULL;
+      default:
+        throw new Unreachable("No default value for unexpected type " + type);
     }
-    if (type == factory.byteType) {
-      return DexValueByte.DEFAULT;
-    }
-    if (type == factory.charType) {
-      return DexValueChar.DEFAULT;
-    }
-    if (type == factory.shortType) {
-      return DexValueShort.DEFAULT;
-    }
-    if (type == factory.intType) {
-      return DexValueInt.DEFAULT;
-    }
-    if (type == factory.longType) {
-      return DexValueLong.DEFAULT;
-    }
-    if (type == factory.floatType) {
-      return DexValueFloat.DEFAULT;
-    }
-    if (type == factory.doubleType) {
-      return DexValueDouble.DEFAULT;
-    }
-    if (type.isArrayType() || type.isClassType()) {
-      return DexValueNull.NULL;
-    }
-    throw new Unreachable("No default value for unexpected type " + type);
   }
 
   public abstract Object getBoxedValue();
@@ -134,8 +128,8 @@
     return null;
   }
 
-  public boolean isDefault(DexType type, DexItemFactory factory) {
-    return this == defaultForType(type, factory);
+  public boolean isDefault(DexType type) {
+    return this == defaultForType(type);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 32cc10b..bef2f57 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -3,14 +3,54 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
 import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
 import static org.objectweb.asm.Opcodes.ACC_DEPRECATED;
 import static org.objectweb.asm.Opcodes.ASM6;
 
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.cf.code.CfArrayLength;
+import com.android.tools.r8.cf.code.CfArrayLoad;
+import com.android.tools.r8.cf.code.CfArrayStore;
+import com.android.tools.r8.cf.code.CfBinop;
+import com.android.tools.r8.cf.code.CfCheckCast;
+import com.android.tools.r8.cf.code.CfConstClass;
+import com.android.tools.r8.cf.code.CfConstMethodHandle;
+import com.android.tools.r8.cf.code.CfConstMethodType;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+import com.android.tools.r8.cf.code.CfGoto;
+import com.android.tools.r8.cf.code.CfIf;
+import com.android.tools.r8.cf.code.CfIfCmp;
+import com.android.tools.r8.cf.code.CfIinc;
+import com.android.tools.r8.cf.code.CfInstanceOf;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfMonitor;
+import com.android.tools.r8.cf.code.CfMultiANewArray;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfNewArray;
+import com.android.tools.r8.cf.code.CfNop;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.cf.code.CfSwitch;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.cf.code.CfUnop;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.CfCode.LocalVariableInfo;
 import com.android.tools.r8.graph.DexValue.DexValueAnnotation;
 import com.android.tools.r8.graph.DexValue.DexValueArray;
 import com.android.tools.r8.graph.DexValue.DexValueBoolean;
@@ -26,12 +66,22 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.graph.JarCode.ReparseContext;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.Monitor;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.InternalOptions;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -40,8 +90,10 @@
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.TypePath;
 
@@ -52,6 +104,8 @@
 
   // Hidden ASM "synthetic attribute" bit we need to clear.
   private static final int ACC_SYNTHETIC_ATTRIBUTE = 0x40000;
+  // Descriptor used by ASM for missing annotations.
+  public static final String SYNTHETIC_ANNOTATION = "Ljava/lang/Synthetic;";
 
   private final JarApplicationReader application;
   private final Consumer<DexClass> classConsumer;
@@ -64,8 +118,12 @@
 
   public void read(Origin origin, ClassKind classKind, InputStream input) throws IOException {
     ClassReader reader = new ClassReader(input);
-    reader.accept(new CreateDexClassVisitor(
-        origin, classKind, reader.b, application, classConsumer), SKIP_FRAMES);
+    int flags = SKIP_FRAMES;
+    if (application.options.enableCfFrontend) {
+      flags = EXPAND_FRAMES;
+    }
+    reader.accept(
+        new CreateDexClassVisitor(origin, classKind, reader.b, application, classConsumer), flags);
   }
 
   private static int cleanAccessFlags(int access) {
@@ -210,6 +268,9 @@
     @Override
     public MethodVisitor visitMethod(
         int access, String name, String desc, String signature, String[] exceptions) {
+      if (application.options.enableCfFrontend) {
+        return new CfCreateMethodVisitor(access, name, desc, signature, exceptions, this);
+      }
       return new CreateMethodVisitor(access, name, desc, signature, exceptions, this);
     }
 
@@ -344,7 +405,7 @@
 
     private DexValue getStaticValue(Object value, DexType type) {
       if (value == null) {
-        return DexValue.defaultForType(type, parent.application.getFactory());
+        return null;
       }
       DexItemFactory factory = parent.application.getFactory();
       if (type == factory.booleanType) {
@@ -396,7 +457,7 @@
     private final int access;
     private final String name;
     private final String desc;
-    private final CreateDexClassVisitor parent;
+    final CreateDexClassVisitor parent;
     private final int parameterCount;
     private List<DexAnnotation> annotations = null;
     private DexValue defaultAnnotation = null;
@@ -404,6 +465,9 @@
     private List<List<DexAnnotation>> parameterAnnotations = null;
     private List<DexValue> parameterNames = null;
     private List<DexValue> parameterFlags = null;
+    final DexMethod method;
+    final MethodAccessFlags flags;
+    Code code = null;
 
     public CreateMethodVisitor(int access, String name, String desc, String signature,
         String[] exceptions, CreateDexClassVisitor parent) {
@@ -412,6 +476,8 @@
       this.name = name;
       this.desc = desc;
       this.parent = parent;
+      this.method = parent.application.getMethod(parent.type, name, desc);
+      this.flags = createMethodAccessFlags(name, access);
       parameterCount = JarApplicationReader.getArgumentCount(desc);
       if (exceptions != null && exceptions.length > 0) {
         DexValue[] values = new DexValue[exceptions.length];
@@ -456,7 +522,7 @@
       // with that descriptor. If javac is fixed, the ASM workaround will not be hit and we will
       // never see this non-existing annotation descriptor. ASM uses the same check to make
       // sure to undo their workaround for the javac bug in their MethodWriter.
-      if (desc.equals("Ljava/lang/Synthetic;")) {
+      if (desc.equals(SYNTHETIC_ANNOTATION)) {
         // We can iterate through all the parameters twice. Once for visible and once for
         // invisible parameter annotations. We only record the number of fake parameter
         // annotations once.
@@ -511,15 +577,17 @@
     }
 
     @Override
-    public void visitEnd() {
-      DexMethod method = parent.application.getMethod(parent.type, name, desc);
-      MethodAccessFlags flags = createMethodAccessFlags(name, access);
-      Code code = null;
-      if (!flags.isAbstract()
-          && !flags.isNative()
-          && parent.classKind == ClassKind.PROGRAM) {
+    public void visitCode() {
+      assert !flags.isAbstract() && !flags.isNative();
+      if (parent.classKind == ClassKind.PROGRAM) {
         code = new JarCode(method, parent.origin, parent.context, parent.application);
       }
+    }
+
+    @Override
+    public void visitEnd() {
+      assert flags.isAbstract() || flags.isNative() || parent.classKind != ClassKind.PROGRAM
+          || code != null;
       DexAnnotationSetRefList parameterAnnotationSets;
       if (parameterAnnotations == null) {
         parameterAnnotationSets = DexAnnotationSetRefList.empty();
@@ -528,7 +596,7 @@
         for (int i = 0; i < parameterAnnotations.size(); i++) {
           sets[i] = createAnnotationSet(parameterAnnotations.get(i));
         }
-        parameterAnnotationSets = new DexAnnotationSetRefList(sets);
+        parameterAnnotationSets = new DexAnnotationSetRefList(sets, fakeParameterAnnotations);
       }
       InternalOptions internalOptions = parent.application.options;
       if (parameterNames != null && internalOptions.canUseParameterNameAnnotations()) {
@@ -566,6 +634,646 @@
     }
   }
 
+  private static class CfCreateMethodVisitor extends CreateMethodVisitor {
+
+    private final JarApplicationReader application;
+    private final DexItemFactory factory;
+    private int maxStack;
+    private int maxLocals;
+    private List<CfInstruction> instructions;
+    private List<CfTryCatch> tryCatchRanges;
+    private List<LocalVariableInfo> localVariables;
+    private Map<Label, CfLabel> labelMap;
+
+    public CfCreateMethodVisitor(
+        int access,
+        String name,
+        String desc,
+        String signature,
+        String[] exceptions,
+        CreateDexClassVisitor parent) {
+      super(access, name, desc, signature, exceptions, parent);
+      this.application = parent.application;
+      this.factory = application.getFactory();
+    }
+
+    @Override
+    public void visitCode() {
+      assert !flags.isAbstract() && !flags.isNative();
+      maxStack = 0;
+      maxLocals = 0;
+      instructions = new ArrayList<>();
+      tryCatchRanges = new ArrayList<>();
+      localVariables = new ArrayList<>();
+      labelMap = new IdentityHashMap<>();
+    }
+
+    @Override
+    public void visitEnd() {
+      if (!flags.isAbstract() && !flags.isNative() && parent.classKind == ClassKind.PROGRAM) {
+        code =
+            new CfCode(method, maxStack, maxLocals, instructions, tryCatchRanges, localVariables);
+      }
+      super.visitEnd();
+    }
+
+    @Override
+    public void visitFrame(
+        int frameType, int nLocals, Object[] localTypes, int nStack, Object[] stackTypes) {
+      assert frameType == Opcodes.F_NEW;
+      Int2ReferenceSortedMap<FrameType> parsedLocals = parseLocals(nLocals, localTypes);
+      List<FrameType> parsedStack = parseStack(nStack, stackTypes);
+      instructions.add(new CfFrame(parsedLocals, parsedStack));
+    }
+
+    private Int2ReferenceSortedMap<FrameType> parseLocals(int typeCount, Object[] asmTypes) {
+      Int2ReferenceSortedMap<FrameType> types = new Int2ReferenceAVLTreeMap<>();
+      int i = 0;
+      for (int j = 0; j < typeCount; j++) {
+        Object localType = asmTypes[j];
+        FrameType value = getFrameType(localType);
+        types.put(i++, value);
+        if (value.isWide()) {
+          i++;
+        }
+      }
+      return types;
+    }
+
+    private List<FrameType> parseStack(int nStack, Object[] stackTypes) {
+      List<FrameType> dexStack = new ArrayList<>(nStack);
+      for (int i = 0; i < nStack; i++) {
+        dexStack.add(getFrameType(stackTypes[i]));
+      }
+      return dexStack;
+    }
+
+    private FrameType getFrameType(Object localType) {
+      if (localType instanceof Label) {
+        return FrameType.uninitializedNew(getLabel((Label) localType));
+      } else if (localType == Opcodes.UNINITIALIZED_THIS) {
+        return FrameType.uninitializedThis();
+      } else if (localType == null || localType == Opcodes.TOP) {
+        return FrameType.top();
+      } else {
+        return FrameType.initialized(parseAsmType(localType));
+      }
+    }
+
+    private CfLabel getLabel(Label label) {
+      return labelMap.computeIfAbsent(label, l -> new CfLabel());
+    }
+
+    private DexType parseAsmType(Object local) {
+      assert local != null && local != Opcodes.TOP;
+      if (local == Opcodes.INTEGER) {
+        return factory.intType;
+      } else if (local == Opcodes.FLOAT) {
+        return factory.floatType;
+      } else if (local == Opcodes.LONG) {
+        return factory.longType;
+      } else if (local == Opcodes.DOUBLE) {
+        return factory.doubleType;
+      } else if (local == Opcodes.NULL) {
+        return DexItemFactory.nullValueType;
+      } else if (local instanceof String) {
+        return createTypeFromInternalType((String) local);
+      } else {
+        throw new Unreachable("Unexpected ASM type: " + local);
+      }
+    }
+
+    private DexType createTypeFromInternalType(String local) {
+      assert local.indexOf('.') == -1;
+      return factory.createType("L" + local + ";");
+    }
+
+    @Override
+    public void visitInsn(int opcode) {
+      switch (opcode) {
+        case Opcodes.NOP:
+          instructions.add(new CfNop());
+          break;
+        case Opcodes.ACONST_NULL:
+          instructions.add(new CfConstNull());
+          break;
+        case Opcodes.ICONST_M1:
+        case Opcodes.ICONST_0:
+        case Opcodes.ICONST_1:
+        case Opcodes.ICONST_2:
+        case Opcodes.ICONST_3:
+        case Opcodes.ICONST_4:
+        case Opcodes.ICONST_5:
+          instructions.add(new CfConstNumber(opcode - Opcodes.ICONST_0, ValueType.INT));
+          break;
+        case Opcodes.LCONST_0:
+        case Opcodes.LCONST_1:
+          instructions.add(new CfConstNumber(opcode - Opcodes.LCONST_0, ValueType.LONG));
+          break;
+        case Opcodes.FCONST_0:
+        case Opcodes.FCONST_1:
+        case Opcodes.FCONST_2:
+          instructions.add(
+              new CfConstNumber(
+                  Float.floatToRawIntBits(opcode - Opcodes.FCONST_0), ValueType.FLOAT));
+          break;
+        case Opcodes.DCONST_0:
+        case Opcodes.DCONST_1:
+          instructions.add(
+              new CfConstNumber(
+                  Double.doubleToRawLongBits(opcode - Opcodes.DCONST_0), ValueType.DOUBLE));
+          break;
+        case Opcodes.IALOAD:
+        case Opcodes.LALOAD:
+        case Opcodes.FALOAD:
+        case Opcodes.DALOAD:
+        case Opcodes.AALOAD:
+        case Opcodes.BALOAD:
+        case Opcodes.CALOAD:
+        case Opcodes.SALOAD:
+          instructions.add(new CfArrayLoad(getMemberTypeForOpcode(opcode)));
+          break;
+        case Opcodes.IASTORE:
+        case Opcodes.LASTORE:
+        case Opcodes.FASTORE:
+        case Opcodes.DASTORE:
+        case Opcodes.AASTORE:
+        case Opcodes.BASTORE:
+        case Opcodes.CASTORE:
+        case Opcodes.SASTORE:
+          instructions.add(new CfArrayStore(getMemberTypeForOpcode(opcode)));
+          break;
+        case Opcodes.POP:
+        case Opcodes.POP2:
+        case Opcodes.DUP:
+        case Opcodes.DUP_X1:
+        case Opcodes.DUP_X2:
+        case Opcodes.DUP2:
+        case Opcodes.DUP2_X1:
+        case Opcodes.DUP2_X2:
+        case Opcodes.SWAP:
+          instructions.add(CfStackInstruction.fromAsm(opcode));
+          break;
+        case Opcodes.IADD:
+        case Opcodes.LADD:
+        case Opcodes.FADD:
+        case Opcodes.DADD:
+        case Opcodes.ISUB:
+        case Opcodes.LSUB:
+        case Opcodes.FSUB:
+        case Opcodes.DSUB:
+        case Opcodes.IMUL:
+        case Opcodes.LMUL:
+        case Opcodes.FMUL:
+        case Opcodes.DMUL:
+        case Opcodes.IDIV:
+        case Opcodes.LDIV:
+        case Opcodes.FDIV:
+        case Opcodes.DDIV:
+        case Opcodes.IREM:
+        case Opcodes.LREM:
+        case Opcodes.FREM:
+        case Opcodes.DREM:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.INEG:
+        case Opcodes.LNEG:
+        case Opcodes.FNEG:
+        case Opcodes.DNEG:
+          instructions.add(new CfUnop(opcode));
+          break;
+        case Opcodes.ISHL:
+        case Opcodes.LSHL:
+        case Opcodes.ISHR:
+        case Opcodes.LSHR:
+        case Opcodes.IUSHR:
+        case Opcodes.LUSHR:
+        case Opcodes.IAND:
+        case Opcodes.LAND:
+        case Opcodes.IOR:
+        case Opcodes.LOR:
+        case Opcodes.IXOR:
+        case Opcodes.LXOR:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.I2L:
+        case Opcodes.I2F:
+        case Opcodes.I2D:
+        case Opcodes.L2I:
+        case Opcodes.L2F:
+        case Opcodes.L2D:
+        case Opcodes.F2I:
+        case Opcodes.F2L:
+        case Opcodes.F2D:
+        case Opcodes.D2I:
+        case Opcodes.D2L:
+        case Opcodes.D2F:
+        case Opcodes.I2B:
+        case Opcodes.I2C:
+        case Opcodes.I2S:
+          instructions.add(new CfUnop(opcode));
+          break;
+        case Opcodes.LCMP:
+        case Opcodes.FCMPL:
+        case Opcodes.FCMPG:
+        case Opcodes.DCMPL:
+        case Opcodes.DCMPG:
+          instructions.add(new CfBinop(opcode));
+          break;
+        case Opcodes.IRETURN:
+          instructions.add(new CfReturn(ValueType.INT));
+          break;
+        case Opcodes.LRETURN:
+          instructions.add(new CfReturn(ValueType.LONG));
+          break;
+        case Opcodes.FRETURN:
+          instructions.add(new CfReturn(ValueType.FLOAT));
+          break;
+        case Opcodes.DRETURN:
+          instructions.add(new CfReturn(ValueType.DOUBLE));
+          break;
+        case Opcodes.ARETURN:
+          instructions.add(new CfReturn(ValueType.OBJECT));
+          break;
+        case Opcodes.RETURN:
+          instructions.add(new CfReturnVoid());
+          break;
+        case Opcodes.ARRAYLENGTH:
+          instructions.add(new CfArrayLength());
+          break;
+        case Opcodes.ATHROW:
+          instructions.add(new CfThrow());
+          break;
+        case Opcodes.MONITORENTER:
+          instructions.add(new CfMonitor(Monitor.Type.ENTER));
+          break;
+        case Opcodes.MONITOREXIT:
+          instructions.add(new CfMonitor(Monitor.Type.EXIT));
+          break;
+        default:
+          throw new Unreachable("Unknown instruction");
+      }
+    }
+
+    private DexType opType(int opcode, DexItemFactory factory) {
+      switch (opcode) {
+        case Opcodes.IADD:
+        case Opcodes.ISUB:
+        case Opcodes.IMUL:
+        case Opcodes.IDIV:
+        case Opcodes.IREM:
+        case Opcodes.INEG:
+        case Opcodes.ISHL:
+        case Opcodes.ISHR:
+        case Opcodes.IUSHR:
+          return factory.intType;
+        case Opcodes.LADD:
+        case Opcodes.LSUB:
+        case Opcodes.LMUL:
+        case Opcodes.LDIV:
+        case Opcodes.LREM:
+        case Opcodes.LNEG:
+        case Opcodes.LSHL:
+        case Opcodes.LSHR:
+        case Opcodes.LUSHR:
+          return factory.longType;
+        case Opcodes.FADD:
+        case Opcodes.FSUB:
+        case Opcodes.FMUL:
+        case Opcodes.FDIV:
+        case Opcodes.FREM:
+        case Opcodes.FNEG:
+          return factory.floatType;
+        case Opcodes.DADD:
+        case Opcodes.DSUB:
+        case Opcodes.DMUL:
+        case Opcodes.DDIV:
+        case Opcodes.DREM:
+        case Opcodes.DNEG:
+          return factory.doubleType;
+        default:
+          throw new Unreachable("Unexpected opcode " + opcode);
+      }
+    }
+
+    private static MemberType getMemberTypeForOpcode(int opcode) {
+      switch (opcode) {
+        case Opcodes.IALOAD:
+        case Opcodes.IASTORE:
+          return MemberType.INT;
+        case Opcodes.FALOAD:
+        case Opcodes.FASTORE:
+          return MemberType.FLOAT;
+        case Opcodes.LALOAD:
+        case Opcodes.LASTORE:
+          return MemberType.LONG;
+        case Opcodes.DALOAD:
+        case Opcodes.DASTORE:
+          return MemberType.DOUBLE;
+        case Opcodes.AALOAD:
+        case Opcodes.AASTORE:
+          return MemberType.OBJECT;
+        case Opcodes.BALOAD:
+        case Opcodes.BASTORE:
+          return MemberType.BOOLEAN; // TODO: Distinguish byte and boolean.
+        case Opcodes.CALOAD:
+        case Opcodes.CASTORE:
+          return MemberType.CHAR;
+        case Opcodes.SALOAD:
+        case Opcodes.SASTORE:
+          return MemberType.SHORT;
+        default:
+          throw new Unreachable("Unexpected array opcode " + opcode);
+      }
+    }
+
+    @Override
+    public void visitIntInsn(int opcode, int operand) {
+      switch (opcode) {
+        case Opcodes.SIPUSH:
+        case Opcodes.BIPUSH:
+          instructions.add(new CfConstNumber(operand, ValueType.INT));
+          break;
+        case Opcodes.NEWARRAY:
+          instructions.add(
+              new CfNewArray(factory.createArrayType(1, arrayTypeDesc(operand, factory))));
+          break;
+        default:
+          throw new Unreachable("Unexpected int opcode " + opcode);
+      }
+    }
+
+    private static DexType arrayTypeDesc(int arrayTypeCode, DexItemFactory factory) {
+      switch (arrayTypeCode) {
+        case Opcodes.T_BOOLEAN:
+          return factory.booleanType;
+        case Opcodes.T_CHAR:
+          return factory.charType;
+        case Opcodes.T_FLOAT:
+          return factory.floatType;
+        case Opcodes.T_DOUBLE:
+          return factory.doubleType;
+        case Opcodes.T_BYTE:
+          return factory.byteType;
+        case Opcodes.T_SHORT:
+          return factory.shortType;
+        case Opcodes.T_INT:
+          return factory.intType;
+        case Opcodes.T_LONG:
+          return factory.longType;
+        default:
+          throw new Unreachable("Unexpected array-type code " + arrayTypeCode);
+      }
+    }
+
+    @Override
+    public void visitVarInsn(int opcode, int var) {
+      ValueType type;
+      switch (opcode) {
+        case Opcodes.ILOAD:
+        case Opcodes.ISTORE:
+          type = ValueType.INT;
+          break;
+        case Opcodes.FLOAD:
+        case Opcodes.FSTORE:
+          type = ValueType.FLOAT;
+          break;
+        case Opcodes.LLOAD:
+        case Opcodes.LSTORE:
+          type = ValueType.LONG;
+          break;
+        case Opcodes.DLOAD:
+        case Opcodes.DSTORE:
+          type = ValueType.DOUBLE;
+          break;
+        case Opcodes.ALOAD:
+        case Opcodes.ASTORE:
+          type = ValueType.OBJECT;
+          break;
+        case Opcodes.RET:
+          throw new Unreachable("RET should be handled by the ASM jsr inliner");
+        default:
+          throw new Unreachable("Unexpected VarInsn opcode: " + opcode);
+      }
+      if (Opcodes.ILOAD <= opcode && opcode <= Opcodes.ALOAD) {
+        instructions.add(new CfLoad(type, var));
+      } else {
+        instructions.add(new CfStore(type, var));
+      }
+    }
+
+    @Override
+    public void visitTypeInsn(int opcode, String typeName) {
+      DexType type = createTypeFromInternalType(typeName);
+      switch (opcode) {
+        case Opcodes.NEW:
+          instructions.add(new CfNew(type));
+          break;
+        case Opcodes.ANEWARRAY:
+          instructions.add(new CfNewArray(factory.createArrayType(1, type)));
+          break;
+        case Opcodes.CHECKCAST:
+          instructions.add(new CfCheckCast(type));
+          break;
+        case Opcodes.INSTANCEOF:
+          instructions.add(new CfInstanceOf(type));
+          break;
+        default:
+          throw new Unreachable("Unexpected TypeInsn opcode: " + opcode);
+      }
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      DexField field =
+          factory.createField(createTypeFromInternalType(owner), factory.createType(desc), name);
+      // TODO(mathiasr): Don't require CfFieldInstruction::declaringField. It is needed for proper
+      // renaming in the backend, but it is not available here in the frontend.
+      instructions.add(new CfFieldInstruction(opcode, field, field));
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
+      visitMethodInsn(opcode, owner, name, desc, false);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      DexMethod method = application.getMethod(owner, name, desc);
+      instructions.add(new CfInvoke(opcode, method, itf));
+    }
+
+    @Override
+    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+      DexCallSite callSite =
+          DexCallSite.fromAsmInvokeDynamic(application, parent.type, name, desc, bsm, bsmArgs);
+      instructions.add(new CfInvokeDynamic(callSite));
+    }
+
+    @Override
+    public void visitJumpInsn(int opcode, Label label) {
+      CfLabel target = getLabel(label);
+      if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
+        if (opcode <= Opcodes.IFLE) {
+          // IFEQ, IFNE, IFLT, IFGE, IFGT, or IFLE.
+          instructions.add(new CfIf(ifType(opcode), ValueType.INT, target));
+        } else {
+          // IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, or
+          // IF_ACMPNE.
+          ValueType valueType;
+          if (opcode <= Opcodes.IF_ICMPLE) {
+            valueType = ValueType.INT;
+          } else {
+            valueType = ValueType.OBJECT;
+          }
+          instructions.add(new CfIfCmp(ifType(opcode), valueType, target));
+        }
+      } else {
+        // GOTO, JSR, IFNULL or IFNONNULL.
+        switch (opcode) {
+          case Opcodes.GOTO:
+            instructions.add(new CfGoto(target));
+            break;
+          case Opcodes.IFNULL:
+          case Opcodes.IFNONNULL:
+            If.Type type = opcode == Opcodes.IFNULL ? If.Type.EQ : If.Type.NE;
+            instructions.add(new CfIf(type, ValueType.OBJECT, target));
+            break;
+          case Opcodes.JSR:
+            throw new Unreachable("JSR should be handled by the ASM jsr inliner");
+          default:
+            throw new Unreachable("Unexpected JumpInsn opcode: " + opcode);
+        }
+      }
+    }
+
+    private static If.Type ifType(int opcode) {
+      switch (opcode) {
+        case Opcodes.IFEQ:
+        case Opcodes.IF_ICMPEQ:
+        case Opcodes.IF_ACMPEQ:
+          return If.Type.EQ;
+        case Opcodes.IFNE:
+        case Opcodes.IF_ICMPNE:
+        case Opcodes.IF_ACMPNE:
+          return If.Type.NE;
+        case Opcodes.IFLT:
+        case Opcodes.IF_ICMPLT:
+          return If.Type.LT;
+        case Opcodes.IFGE:
+        case Opcodes.IF_ICMPGE:
+          return If.Type.GE;
+        case Opcodes.IFGT:
+        case Opcodes.IF_ICMPGT:
+          return If.Type.GT;
+        case Opcodes.IFLE:
+        case Opcodes.IF_ICMPLE:
+          return If.Type.LE;
+        default:
+          throw new Unreachable("Unexpected If instruction opcode: " + opcode);
+      }
+    }
+
+    @Override
+    public void visitLabel(Label label) {
+      instructions.add(getLabel(label));
+    }
+
+    @Override
+    public void visitLdcInsn(Object cst) {
+      if (cst instanceof Type) {
+        Type type = (Type) cst;
+        if (type.getSort() == Type.METHOD) {
+          DexProto proto = application.getProto(type.getDescriptor());
+          instructions.add(new CfConstMethodType(proto));
+        } else {
+          instructions.add(new CfConstClass(factory.createType(type.getDescriptor())));
+        }
+      } else if (cst instanceof String) {
+        instructions.add(new CfConstString(factory.createString((String) cst)));
+      } else if (cst instanceof Long) {
+        instructions.add(new CfConstNumber((Long) cst, ValueType.LONG));
+      } else if (cst instanceof Double) {
+        long l = Double.doubleToRawLongBits((Double) cst);
+        instructions.add(new CfConstNumber(l, ValueType.DOUBLE));
+      } else if (cst instanceof Integer) {
+        instructions.add(new CfConstNumber((Integer) cst, ValueType.INT));
+      } else if (cst instanceof Float) {
+        long i = Float.floatToRawIntBits((Float) cst);
+        instructions.add(new CfConstNumber(i, ValueType.FLOAT));
+      } else if (cst instanceof Handle) {
+        instructions.add(
+            new CfConstMethodHandle(
+                DexMethodHandle.fromAsmHandle((Handle) cst, application, parent.type)));
+      } else {
+        throw new CompilationError("Unsupported constant: " + cst.toString());
+      }
+    }
+
+    @Override
+    public void visitIincInsn(int var, int increment) {
+      instructions.add(new CfIinc(var, increment));
+    }
+
+    @Override
+    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
+      assert max == min + labels.length - 1;
+      ArrayList<CfLabel> targets = new ArrayList<>(labels.length);
+      for (Label label : labels) {
+        targets.add(getLabel(label));
+      }
+      instructions.add(new CfSwitch(CfSwitch.Kind.TABLE, getLabel(dflt), new int[] {min}, targets));
+    }
+
+    @Override
+    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
+      ArrayList<CfLabel> targets = new ArrayList<>(labels.length);
+      for (Label label : labels) {
+        targets.add(getLabel(label));
+      }
+      instructions.add(new CfSwitch(CfSwitch.Kind.LOOKUP, getLabel(dflt), keys, targets));
+    }
+
+    @Override
+    public void visitMultiANewArrayInsn(String desc, int dims) {
+      instructions.add(new CfMultiANewArray(factory.createType(desc), dims));
+    }
+
+    @Override
+    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
+      List<DexType> guards =
+          Collections.singletonList(
+              type == null ? DexItemFactory.catchAllType : createTypeFromInternalType(type));
+      List<CfLabel> targets = Collections.singletonList(getLabel(handler));
+      tryCatchRanges.add(new CfTryCatch(getLabel(start), getLabel(end), guards, targets));
+    }
+
+    @Override
+    public void visitLocalVariable(
+        String name, String desc, String signature, Label start, Label end, int index) {
+      DebugLocalInfo debugLocalInfo =
+          new DebugLocalInfo(
+              factory.createString(name),
+              factory.createType(desc),
+              signature == null ? null : factory.createString(signature));
+      localVariables.add(
+          new LocalVariableInfo(index, debugLocalInfo, getLabel(start), getLabel(end)));
+    }
+
+    @Override
+    public void visitLineNumber(int line, Label start) {
+      instructions.add(new CfPosition(getLabel(start), new Position(line, null, method, null)));
+    }
+
+    @Override
+    public void visitMaxs(int maxStack, int maxLocals) {
+      assert maxStack >= 0;
+      assert maxLocals >= 0;
+      this.maxStack = maxStack;
+      this.maxLocals = maxLocals;
+    }
+  }
+
   private static class CreateAnnotationVisitor extends AnnotationVisitor {
 
     private final JarApplicationReader application;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 24e6eb5..fb75931 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -42,6 +42,22 @@
 
   private Int2ReferenceMap<DebugLocalInfo> localsAtEntry;
 
+  public boolean consistentBlockInstructions(boolean argumentsAllowed) {
+    for (Instruction instruction : getInstructions()) {
+      assert instruction.getPosition() != null;
+      assert instruction.getBlock() == this;
+      assert !instruction.isArgument() || argumentsAllowed;
+      assert !instruction.isDebugLocalRead() || !instruction.getDebugValues().isEmpty();
+      // TODO(b/79186787): Ensure DEX backend inserts Move *after* arguments.
+      if (!(instruction.isArgument()
+          || instruction.isMove()
+          || instruction.isDebugLocalsChange())) {
+        argumentsAllowed = false;
+      }
+    }
+    return true;
+  }
+
   public void setLocalsAtEntry(Int2ReferenceMap<DebugLocalInfo> localsAtEntry) {
     this.localsAtEntry = localsAtEntry;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index f179b9e..1a43128 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -49,16 +49,20 @@
     // if the register allocator could not put input and output in the same register
     // we have to insert a move before the check cast instruction.
     int inRegister = builder.allocatedRegister(inValues.get(0), getNumber());
-    int outRegister = builder.allocatedRegister(outValue, getNumber());
-    if (inRegister == outRegister) {
-      builder.add(this, new com.android.tools.r8.code.CheckCast(outRegister, type));
+    if (outValue == null) {
+      builder.add(this, new com.android.tools.r8.code.CheckCast(inRegister, type));
     } else {
-      com.android.tools.r8.code.CheckCast cast =
-          new com.android.tools.r8.code.CheckCast(outRegister, type);
-      if (outRegister <= Constants.U4BIT_MAX && inRegister <= Constants.U4BIT_MAX) {
-        builder.add(this, new MoveObject(outRegister, inRegister), cast);
+      int outRegister = builder.allocatedRegister(outValue, getNumber());
+      if (inRegister == outRegister) {
+        builder.add(this, new com.android.tools.r8.code.CheckCast(outRegister, type));
       } else {
-        builder.add(this, new MoveObjectFrom16(outRegister, inRegister), cast);
+        com.android.tools.r8.code.CheckCast cast =
+            new com.android.tools.r8.code.CheckCast(outRegister, type);
+        if (outRegister <= Constants.U4BIT_MAX && inRegister <= Constants.U4BIT_MAX) {
+          builder.add(this, new MoveObject(outRegister, inRegister), cast);
+        } else {
+          builder.add(this, new MoveObjectFrom16(outRegister, inRegister), cast);
+        }
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index c728e6a..b763298 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -65,7 +65,7 @@
   @Override
   public String toString() {
     if (getBlock() != null && !getBlock().getSuccessors().isEmpty()) {
-      return super.toString() + "block " + getTarget().getNumber();
+      return super.toString() + "block " + blockNumberToString(getTarget());
     }
     return super.toString() + "block <unknown>";
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index beaa62b..bb13495 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -125,7 +125,8 @@
         }
       }
     }
-    assert liveAtEntrySets.get(sorted.get(0)).size() == 0;
+    assert liveAtEntrySets.get(sorted.get(0)).size() == 0
+        : "Unexpected values live at entry to first block: " + liveAtEntrySets.get(sorted.get(0));
     return liveAtEntrySets;
   }
 
@@ -494,11 +495,10 @@
   }
 
   private boolean consistentBlockInstructions() {
+    boolean argumentsAllowed = true;
     for (BasicBlock block : blocks) {
-      for (Instruction instruction : block.getInstructions()) {
-        assert instruction.getPosition() != null;
-        assert instruction.getBlock() == block;
-      }
+      block.consistentBlockInstructions(argumentsAllowed);
+      argumentsAllowed = false;
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java
index 001b648..0235a14 100644
--- a/src/main/java/com/android/tools/r8/ir/code/If.java
+++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -126,8 +126,14 @@
 
   @Override
   public String toString() {
-    return super.toString() + " " + type + " block " + getTrueTarget().getNumber()
-        + " (fallthrough " + fallthroughBlock().getNumber() + ")";
+    return super.toString()
+        + " "
+        + type
+        + " block "
+        + blockNumberToString(getTrueTarget())
+        + " (fallthrough "
+        + blockNumberToString(fallthroughBlock())
+        + ")";
   }
 
   @Override
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 1084bef..1c3423c 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
@@ -26,6 +26,7 @@
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
@@ -164,11 +165,16 @@
 
   public void replaceDebugValue(Value oldValue, Value newValue) {
     if (debugValues.remove(oldValue)) {
+      // TODO(mathiasr): Enable this assertion when BasicBlock has current position so trivial phi
+      // removal can take local info into account.
+      // assert newValue.getLocalInfo() == oldValue.getLocalInfo()
+      //     : "Replacing debug values with inconsistent locals " +
+      //       oldValue.getLocalInfo() + " and " + newValue.getLocalInfo() +
+      //       ". This is likely a code transformation bug " +
+      //       "that has not taken local information into account";
       if (newValue.hasLocalInfo()) {
-        // TODO(zerny): Insert a write if replacing a phi with different debug-local info.
         addDebugValue(newValue);
       }
-      // TODO(zerny): Else: Insert a write if replacing a phi with associated debug-local info.
     }
   }
 
@@ -200,6 +206,21 @@
     assert false;
   }
 
+  public Value removeDebugValue(DebugLocalInfo localInfo) {
+    if (debugValues != null) {
+      Iterator<Value> it = debugValues.iterator();
+      while (it.hasNext()) {
+        Value value = it.next();
+        if (value.hasLocalInfo() && value.getLocalInfo() == localInfo) {
+          it.remove();
+          value.removeDebugUser(this);
+          return value;
+        }
+      }
+    }
+    return null;
+  }
+
   public void clearDebugValues() {
     if (debugValues != null) {
       for (Value debugValue : debugValues) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
index e365eaa..349d5df 100644
--- a/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/JumpInstruction.java
@@ -50,4 +50,12 @@
   public Constraint inliningConstraint(AppInfoWithLiveness info, DexType invocationContext) {
     return Constraint.ALWAYS;
   }
+
+  static String blockNumberToString(BasicBlock block) {
+    try {
+      return "" + block.getNumber();
+    } catch (AssertionError e) {
+      return "<invalid>";
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index ccac8fb..f27153d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -22,7 +22,9 @@
 
   public Move(Value dest, Value src) {
     super(dest, src);
-    if (src.isNeverNull()) {
+    // CodeRewriter.removeOrReplaceByDebugLocalWrite() might add a Move to a dest that is already
+    // marked never-null. Avoid tripping assertion in markNeverNull() in that case.
+    if (src.isNeverNull() && dest.canBeNull()) {
       dest.markNeverNull();
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Pop.java b/src/main/java/com/android/tools/r8/ir/code/Pop.java
index 9e29aae..70d6af4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Pop.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Pop.java
@@ -4,7 +4,7 @@
 package com.android.tools.r8.ir.code;
 
 import com.android.tools.r8.cf.LoadStoreHelper;
-import com.android.tools.r8.cf.code.CfPop;
+import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.CfBuilder;
@@ -61,7 +61,7 @@
 
   @Override
   public void buildCf(CfBuilder builder) {
-    builder.add(new CfPop(inValues.get(0).type));
+    builder.add(CfStackInstruction.popType(inValues.get(0).type));
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Switch.java b/src/main/java/com/android/tools/r8/ir/code/Switch.java
index e8baacc..50e59f1 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Switch.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Switch.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfSwitch;
+import com.android.tools.r8.cf.code.CfSwitch.Kind;
 import com.android.tools.r8.code.Nop;
 import com.android.tools.r8.code.PackedSwitch;
 import com.android.tools.r8.code.PackedSwitchPayload;
@@ -265,7 +266,7 @@
       builder.append("          ");
       builder.append(getKey(i));
       builder.append(" -> ");
-      builder.append(targetBlock(i).getNumber());
+      builder.append(blockNumberToString(targetBlock(i)));
       builder.append("\n");
     }
     builder.append("          F -> ");
@@ -294,6 +295,6 @@
     for (int index : targetBlockIndices) {
       labels.add(builder.getLabel(successors.get(index)));
     }
-    builder.add(new CfSwitch(builder.getLabel(fallthroughBlock()), keys, labels));
+    builder.add(new CfSwitch(Kind.LOOKUP, builder.getLabel(fallthroughBlock()), keys, labels));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 3cfb0c3..272b565 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -68,6 +68,23 @@
           throw new Unreachable();
       }
     }
+
+    static DebugUse join(DebugUse a, DebugUse b) {
+      if (a == LIVE_FINAL || b == LIVE_FINAL) {
+        return LIVE_FINAL;
+      }
+      if (a == b) {
+        return a;
+      }
+      if (a == LIVE) {
+        return b;
+      }
+      if (b == LIVE) {
+        return a;
+      }
+      assert (a == START && b == END) || (a == END && b == START);
+      return LIVE_FINAL;
+    }
   }
 
   public static final int UNDEFINED_NUMBER = -1;
@@ -393,9 +410,10 @@
       user.replaceOperand(this, newValue);
     }
     if (debugData != null) {
-      for (Instruction user : debugUsers()) {
-        user.replaceDebugValue(this, newValue);
+      for (Entry<Instruction, DebugUse> user : debugData.users.entrySet()) {
+        replaceUserInDebugData(user, newValue);
       }
+      debugData.users.clear();
       for (Phi user : debugPhiUsers()) {
         user.replaceDebugValue(this, newValue);
       }
@@ -435,12 +453,12 @@
       }
     }
     if (debugData != null) {
-      Iterator<Instruction> it = debugData.users.keySet().iterator();
-      while (it.hasNext()) {
-        Instruction user = it.next();
-        if (selectedInstructions.contains(user)) {
-          it.remove();
-          user.replaceDebugValue(this, newValue);
+      Iterator<Entry<Instruction, DebugUse>> users = debugData.users.entrySet().iterator();
+      while (users.hasNext()) {
+        Entry<Instruction, DebugUse> user = users.next();
+        if (selectedInstructions.contains(user.getKey())) {
+          replaceUserInDebugData(user, newValue);
+          users.remove();
         }
       }
       Iterator<Phi> phis = debugData.phiUsers.iterator();
@@ -454,8 +472,27 @@
     }
   }
 
+  private void replaceUserInDebugData(Entry<Instruction, DebugUse> user, Value newValue) {
+    Instruction instruction = user.getKey();
+    DebugUse debugUse = user.getValue();
+    instruction.replaceDebugValue(this, newValue);
+    // If user is a DebugLocalRead and now has no debug values, we would like to remove it.
+    // However, replaceUserInDebugData() is called in contexts where the instruction list is being
+    // iterated, so we cannot remove user from the instruction list at this point.
+    if (newValue.hasLocalInfo()) {
+      DebugUse existing = newValue.debugData.users.get(instruction);
+      assert existing != null;
+      newValue.debugData.users.put(instruction, DebugUse.join(debugUse, existing));
+    }
+  }
+
   public void replaceDebugUser(Instruction oldUser, Instruction newUser) {
     DebugUse use = debugData.users.remove(oldUser);
+    if (use == DebugUse.START && newUser.outValue == this) {
+      // Register allocation requires that debug values are live at the entry to the instruction.
+      // Remove this debug use since it is starting at the instruction that defines it.
+      return;
+    }
     if (use != null) {
       newUser.addDebugValue(this);
       debugData.users.put(newUser, use);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index d945dea..e798fd2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -7,9 +7,7 @@
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFrame;
-import com.android.tools.r8.cf.code.CfFrame.Uninitialized;
-import com.android.tools.r8.cf.code.CfFrame.UninitializedNew;
-import com.android.tools.r8.cf.code.CfFrame.UninitializedThis;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfLabel;
 import com.android.tools.r8.cf.code.CfPosition;
@@ -263,7 +261,8 @@
         if (!tryCatchHandlers.isEmpty()) {
           // Close try-catch and save the range.
           CfLabel tryCatchEnd = getLabel(block);
-          tryCatchRanges.add(new CfTryCatch(tryCatchStart, tryCatchEnd, tryCatchHandlers, this));
+          tryCatchRanges.add(
+              CfTryCatch.fromBuilder(tryCatchStart, tryCatchEnd, tryCatchHandlers, this));
           emitLabel(tryCatchEnd);
         }
         if (!handlers.isEmpty()) {
@@ -439,51 +438,43 @@
     // TODO(zerny): Support having values on the stack on control-edges.
     assert stack.isEmpty();
 
-    List<DexType> stackTypes;
+    List<FrameType> stackTypes;
     if (block.entry().isMoveException()) {
       StackValue exception = (StackValue) block.entry().outValue();
-      stackTypes = Collections.singletonList(exception.getObjectType());
+      stackTypes = Collections.singletonList(FrameType.initialized(exception.getObjectType()));
     } else {
       stackTypes = Collections.emptyList();
     }
 
     Collection<Value> locals = registerAllocator.getLocalsAtBlockEntry(block);
-    Int2ReferenceSortedMap<DexType> mapping = new Int2ReferenceAVLTreeMap<>();
-    Int2ReferenceSortedMap<Uninitialized> allocators = new Int2ReferenceAVLTreeMap<>();
+    Int2ReferenceSortedMap<FrameType> mapping = new Int2ReferenceAVLTreeMap<>();
 
     for (Value local : locals) {
-      DexType type;
-      Uninitialized allocator = null;
-      switch (local.outType()) {
-        case INT:
-          type = factory.intType;
-          break;
-        case FLOAT:
-          type = factory.floatType;
-          break;
-        case LONG:
-          type = factory.longType;
-          break;
-        case DOUBLE:
-          type = factory.doubleType;
-          break;
-        case OBJECT:
-          type = types.get(local);
-          allocator = findAllocator(block, local);
-          break;
-        default:
-          throw new Unreachable(
-              "Unexpected local type: " + local.outType() + " for local: " + local);
-      }
-      mapping.put(getLocalRegister(local), type);
-      if (allocator != null) {
-        allocators.put(getLocalRegister(local), allocator);
-      }
+      mapping.put(getLocalRegister(local), getFrameType(block, local));
     }
-    instructions.add(new CfFrame(mapping, allocators, stackTypes));
+    instructions.add(new CfFrame(mapping, stackTypes));
   }
 
-  private Uninitialized findAllocator(BasicBlock liveBlock, Value value) {
+  private FrameType getFrameType(BasicBlock liveBlock, Value local) {
+    switch (local.outType()) {
+      case INT:
+        return FrameType.initialized(factory.intType);
+      case FLOAT:
+        return FrameType.initialized(factory.floatType);
+      case LONG:
+        return FrameType.initialized(factory.longType);
+      case DOUBLE:
+        return FrameType.initialized(factory.doubleType);
+      case OBJECT:
+        FrameType type = findAllocator(liveBlock, local);
+        return type != null ? type : FrameType.initialized(types.get(local));
+      default:
+        throw new Unreachable(
+            "Unexpected local type: " + local.outType() + " for local: " + local);
+    }
+  }
+
+  private FrameType findAllocator(BasicBlock liveBlock, Value value) {
     Instruction definition = value.definition;
     while (definition != null && (definition.isStore() || definition.isLoad())) {
       definition = definition.inValues().get(0).definition;
@@ -491,13 +482,13 @@
     if (definition == null) {
       return null;
     }
-    Uninitialized res;
+    FrameType res;
     if (definition.isNewInstance()) {
-      res = new UninitializedNew(newInstanceLabels.get(definition.asNewInstance()));
+      res = FrameType.uninitializedNew(newInstanceLabels.get(definition.asNewInstance()));
     } else if (definition.isArgument()
         && method.isInstanceInitializer()
         && definition.outValue().isThis()) {
-      res = new UninitializedThis();
+      res = FrameType.uninitializedThis();
     } else {
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 182bb49..8201562 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -420,10 +420,6 @@
     return registerAllocator.getArgumentOrAllocateRegisterForValue(value, instructionNumber);
   }
 
-  public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
-    return registerAllocator.argumentValueUsesHighRegister(value, instructionNumber);
-  }
-
   public void addGoto(com.android.tools.r8.ir.code.Goto jump) {
     if (jump.getTarget() != nextBlock) {
       add(jump, new GotoInfo(jump));
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 9c6bb86..0d87723 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -120,7 +120,13 @@
   }
 
   @Override
-  public DebugLocalInfo getCurrentLocal(int register) {
+  public DebugLocalInfo getIncomingLocal(int register) {
+    // TODO(zerny): Support locals in the dex front-end. b/36378142
+    return null;
+  }
+
+  @Override
+  public DebugLocalInfo getOutgoingLocal(int register) {
     // TODO(zerny): Support locals in the dex front-end. b/36378142
     return null;
   }
@@ -166,12 +172,6 @@
   }
 
   @Override
-  public void closingCurrentBlockWithFallthrough(
-      int fallthroughInstructionIndex, IRBuilder builder) {
-    // Intentionally empty.
-  }
-
-  @Override
   public void buildInstruction(
       IRBuilder builder, int instructionIndex, boolean firstBlockInstruction)
       throws ApiLevelException {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 5efbfb5..58ed822 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -500,7 +500,6 @@
         }
         BlockInfo info = targets.get(source.instructionOffset(i));
         if (info != null && info.block != currentBlock) {
-          source.closingCurrentBlockWithFallthrough(i, this);
           closeCurrentBlockWithFallThrough(info.block);
           addToWorklist(info.block, i);
           break;
@@ -549,20 +548,20 @@
   }
 
   public void addThisArgument(int register) {
-    DebugLocalInfo local = getCurrentLocal(register);
+    DebugLocalInfo local = getOutgoingLocal(register);
     Value value = writeRegister(register, ValueType.OBJECT, ThrowingInfo.NO_THROW, local);
     addInstruction(new Argument(value));
     value.markAsThis();
   }
 
   public void addNonThisArgument(int register, ValueType valueType) {
-    DebugLocalInfo local = getCurrentLocal(register);
+    DebugLocalInfo local = getOutgoingLocal(register);
     Value value = writeRegister(register, valueType, ThrowingInfo.NO_THROW, local);
     addInstruction(new Argument(value));
   }
 
   public void addBooleanNonThisArgument(int register) {
-    DebugLocalInfo local = getCurrentLocal(register);
+    DebugLocalInfo local = getOutgoingLocal(register);
     Value value = writeRegister(register, ValueType.INT, ThrowingInfo.NO_THROW, local);
     value.setKnownToBeBoolean(true);
     addInstruction(new Argument(value));
@@ -585,10 +584,10 @@
     addInstruction(write);
   }
 
-  private Value getLocalValue(int register, DebugLocalInfo local) {
+  private Value getIncomingLocalValue(int register, DebugLocalInfo local) {
     assert options.debug;
     assert local != null;
-    assert local == getCurrentLocal(register);
+    assert local == getIncomingLocal(register);
     ValueType valueType = ValueType.fromDexType(local.type);
     return readRegisterIgnoreLocal(register, valueType);
   }
@@ -603,7 +602,7 @@
     if (!options.debug) {
       return;
     }
-    Value value = getLocalValue(register, local);
+    Value value = getIncomingLocalValue(register, local);
     if (isValidFor(value, local)) {
       debugLocalReads.add(value);
     }
@@ -613,40 +612,42 @@
     if (!options.debug) {
       return;
     }
-    Value value = getLocalValue(register, local);
+    assert local != null;
+    assert local == getOutgoingLocal(register);
+    ValueType valueType = ValueType.fromDexType(local.type);
+    Value incomingValue = readRegisterIgnoreLocal(register, valueType);
 
-    // If the value is for a different local, introduce the new local. We cannot shortcut if the
-    // local is defined by a phi as it could end up being trivial.
-    if (value.isPhi() || value.getLocalInfo() != local) {
-      addDebugLocalWrite(ValueType.fromDexType(local.type), register, value);
+    // TODO(mathiasr): This can be simplified once trivial phi removal is local-info aware.
+    if (incomingValue.isPhi() || incomingValue.getLocalInfo() != local) {
+      addDebugLocalWrite(ValueType.fromDexType(local.type), register, incomingValue);
       return;
     }
+    assert incomingValue.getLocalInfo() == local;
+    assert !incomingValue.isUninitializedLocal();
 
-    if (!isValidFor(value, local)) {
-      return;
-    }
-
-    // When inserting a start there are two possibilities:
+    // When inserting a start there are three possibilities:
     // 1. The block is empty (eg, instructions from block entry until now materialized to nothing).
-    // 2. The block is non-empty (and the last instruction does not define the local to start).
+    // 2. The block is non-empty and the last instruction defines the local to start.
+    // 3. The block is non-empty and the last instruction does not define the local to start.
     if (currentBlock.getInstructions().isEmpty()) {
       addInstruction(new DebugLocalRead());
     }
     Instruction instruction = currentBlock.getInstructions().getLast();
-    assert instruction.outValue() != value;
-    instruction.addDebugValue(value);
-    value.addDebugLocalStart(instruction);
+    if (instruction.outValue() == incomingValue) {
+      return;
+    }
+    instruction.addDebugValue(incomingValue);
+    incomingValue.addDebugLocalStart(instruction);
   }
 
   public void addDebugLocalEnd(int register, DebugLocalInfo local) {
     if (!options.debug) {
       return;
     }
-    Value value = getLocalValue(register, local);
+    Value value = getIncomingLocalValue(register, local);
     if (!isValidFor(value, local)) {
       return;
     }
-
     // When inserting an end there are three possibilities:
     // 1. The block is empty (eg, instructions from block entry until now materialized to nothing).
     // 2. The block has an instruction not defining the local being ended.
@@ -862,7 +863,7 @@
     Value in = readRegister(src, type);
     if (options.debug) {
       // If the move is writing to a different local we must construct a new value.
-      DebugLocalInfo destLocal = getCurrentLocal(dest);
+      DebugLocalInfo destLocal = getOutgoingLocal(dest);
       if (destLocal != null && destLocal != in.getLocalInfo()) {
         addDebugLocalWrite(type, dest, in);
         return;
@@ -1573,7 +1574,7 @@
   // Value abstraction methods.
 
   public Value readRegister(int register, ValueType type) {
-    DebugLocalInfo local = getCurrentLocal(register);
+    DebugLocalInfo local = getIncomingLocal(register);
     Value value = readRegister(register, type, currentBlock, EdgeType.NON_EDGE, local);
     // Check that any information about a current-local is consistent with the read.
     if (local != null && value.getLocalInfo() != local && !value.isUninitializedLocal()) {
@@ -1591,8 +1592,8 @@
     return value;
   }
 
-  public Value readRegisterIgnoreLocal(int register, ValueType type) {
-    DebugLocalInfo local = getCurrentLocal(register);
+  private Value readRegisterIgnoreLocal(int register, ValueType type) {
+    DebugLocalInfo local = getIncomingLocal(register);
     return readRegister(register, type, currentBlock, EdgeType.NON_EDGE, local);
   }
 
@@ -1691,17 +1692,33 @@
   }
 
   public Value writeRegister(int register, ValueType type, ThrowingInfo throwing) {
-    DebugLocalInfo local = getCurrentLocal(register);
-    previousLocalValue = local == null ? null : readRegisterIgnoreLocal(register, type);
-    return writeRegister(register, type, throwing, local);
+    DebugLocalInfo incomingLocal = getIncomingLocal(register);
+    DebugLocalInfo outgoingLocal = getOutgoingLocal(register);
+    // If the local info does not change at the current instruction, we need to ensure
+    // that the old value is read at the instruction by setting 'previousLocalValue'.
+    // If the local info changes, then there must be both an old local ending
+    // and a new local starting at the current instruction, and it is up to the SourceCode
+    // to ensure that the old local is read when it ends.
+    // Furthermore, if incomingLocal != outgoingLocal, then we cannot be sure that
+    // the type of the incomingLocal is the same as the type of the outgoingLocal,
+    // and we must not call readRegisterIgnoreLocal() with the wrong type.
+    previousLocalValue =
+        (incomingLocal == null || incomingLocal != outgoingLocal)
+            ? null
+            : readRegisterIgnoreLocal(register, type);
+    return writeRegister(register, type, throwing, outgoingLocal);
   }
 
   public Value writeNumericRegister(int register, NumericType type, ThrowingInfo throwing) {
     return writeRegister(register, ValueType.fromNumericType(type), throwing);
   }
 
-  private DebugLocalInfo getCurrentLocal(int register) {
-    return options.debug ? source.getCurrentLocal(register) : null;
+  private DebugLocalInfo getIncomingLocal(int register) {
+    return options.debug ? source.getIncomingLocal(register) : null;
+  }
+
+  private DebugLocalInfo getOutgoingLocal(int register) {
+    return options.debug ? source.getOutgoingLocal(register) : null;
   }
 
   private void checkRegister(int register) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 2b6ad47..f44bc25 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -568,6 +568,10 @@
       Log.debug(getClass(), "Original code for %s:\n%s",
           method.toSourceString(), logCode(options, method));
     }
+    if (options.skipIR) {
+      feedback.markProcessed(method, Constraint.NEVER);
+      return;
+    }
     IRCode code = method.buildIR(options);
     if (code == null) {
       feedback.markProcessed(method, Constraint.NEVER);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
index b5dfdbb..46cc8e9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarSourceCode.java
@@ -271,6 +271,8 @@
   public void buildPrelude(IRBuilder builder) {
     currentPosition = getPreamblePosition();
 
+    state.beginTransactionSynthetic();
+
     // Record types for arguments.
     Int2ReferenceMap<ValueType> argumentLocals = recordArgumentTypes();
     Int2ReferenceMap<ValueType> initializedLocals = new Int2ReferenceOpenHashMap<>(argumentLocals);
@@ -328,13 +330,8 @@
       }
     }
 
-    // TODO(zerny): This is getting a little out of hands. Clean it up.
-
-    // Add debug information for all locals at the initial label.
-    List<Local> locals = null;
-    if (initialLabel != null) {
-      locals = state.openLocals(getOffset(initialLabel));
-    }
+    state.endTransaction();
+    state.beginTransaction(0, true);
 
     // Build the actual argument instructions now that type and debug information is known
     // for arguments.
@@ -344,13 +341,13 @@
       builder.addDebugUninitialized(entry.getIntKey(), entry.getValue());
     }
 
-    if (locals != null) {
-      for (Local local : locals) {
-        if (!argumentLocals.containsKey(local.slot.register)) {
-          builder.addDebugLocalStart(local.slot.register, local.info);
-        }
+    // Add debug information for all locals at the initial label.
+    for (Local local : state.getLocalsToOpen()) {
+      if (!argumentLocals.containsKey(local.slot.register)) {
+        builder.addDebugLocalStart(local.slot.register, local.info);
       }
     }
+    state.endTransaction();
 
     if (generateMethodSynchronization()) {
       generatingMethodSynchronization = true;
@@ -422,33 +419,36 @@
     state.recordStateForTarget(0);
     for (JarStateWorklistItem item = worklist.poll(); item != null; item = worklist.poll()) {
       state.restoreState(item.instructionIndex);
+      state.beginTransactionAtBlockStart(item.instructionIndex);
+      state.endTransaction();
       // Iterate each of the instructions in the block to compute the outgoing JarState.
       int instCount = instructionCount();
-      for (int i = item.instructionIndex; i <= instCount; ++i) {
-        // If we are at the end of the instruction stream or if we have reached the start
-        // of a new block, propagate the state to all successors and add the ones
-        // that changed to the worklist.
-        if (i == instCount || (i != item.instructionIndex && CFG.containsKey(i))) {
-          item.blockInfo.normalSuccessors.iterator().forEachRemaining(offset -> {
-            if (state.recordStateForTarget(offset)) {
-              if (offset >= 0) {
-                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
-              }
-            }
-          });
-          item.blockInfo.exceptionalSuccessors.iterator().forEachRemaining(offset -> {
-            if (state.recordStateForExceptionalTarget(offset)) {
-              if (offset >= 0) {
-                worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
-              }
-            }
-          });
-          break;
-        }
-
+      int blockEnd = item.instructionIndex + 1;
+      while (blockEnd < instCount && !CFG.containsKey(blockEnd)) {
+        blockEnd += 1;
+      }
+      for (int i = item.instructionIndex; i < blockEnd; ++i) {
+        state.beginTransaction(i + 1, i + 1 != blockEnd);
         AbstractInsnNode insn = getInstruction(i);
         updateState(insn);
+        state.endTransaction();
       }
+      // At the end of the current block, propagate the state to all successors and add the ones
+      // that changed to the worklist.
+      item.blockInfo.normalSuccessors.iterator().forEachRemaining(offset -> {
+        if (state.recordStateForTarget(offset)) {
+          if (offset >= 0) {
+            worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+          }
+        }
+      });
+      item.blockInfo.exceptionalSuccessors.iterator().forEachRemaining(offset -> {
+        if (state.recordStateForExceptionalTarget(offset)) {
+          if (offset >= 0) {
+            worklist.add(new JarStateWorklistItem(CFG.get(offset.intValue()), offset));
+          }
+        }
+      });
     }
     state.restoreState(0);
   }
@@ -477,17 +477,6 @@
   }
 
   @Override
-  public void closingCurrentBlockWithFallthrough(
-      int fallthroughInstructionIndex, IRBuilder builder) {
-    AbstractInsnNode insn = node.instructions.get(fallthroughInstructionIndex);
-    if (insn instanceof LabelNode) {
-      for (Local local : state.getLocalsToClose(getOffset(insn))) {
-        builder.addDebugLocalEnd(local.slot.register, local.info);
-      }
-    }
-  }
-
-  @Override
   public void buildInstruction(
       IRBuilder builder, int instructionIndex, boolean firstBlockInstruction)
       throws ApiLevelException {
@@ -518,7 +507,35 @@
       preInstructionState = state.toString();
     }
 
+    if (firstBlockInstruction && insn != initialLabel) {
+      int offset = getOffset(insn);
+      state.beginTransactionAtBlockStart(offset);
+      assert state.getLocalsToClose().isEmpty();
+      for (Local local : state.getLocalsToOpen()) {
+        builder.addDebugLocalStart(local.slot.register, local.info);
+      }
+      state.endTransaction();
+    }
+
+    boolean hasNextInstruction =
+        instructionIndex + 1 != instructionCount()
+            && !builder.getCFG().containsKey(instructionIndex + 1);
+    state.beginTransaction(instructionIndex + 1, hasNextInstruction);
     build(insn, builder);
+    if (hasNextInstruction || !isControlFlowInstruction(insn)) {
+      // We're either in straight-line code or at the end of a fallthrough block.
+      // Close locals starting at this point.
+      for (Local local : state.getLocalsToClose()) {
+        builder.addDebugLocalEnd(local.slot.register, local.info);
+      }
+    }
+    if (hasNextInstruction) {
+      // Open the scope of locals starting at this point.
+      for (Local local : state.getLocalsToOpen()) {
+        builder.addDebugLocalStart(local.slot.register, local.info);
+      }
+    }
+    state.endTransaction();
 
     if (Log.ENABLED && !(insn instanceof LineNumberNode)) {
       int offset = getOffset(insn);
@@ -555,8 +572,13 @@
   }
 
   @Override
-  public DebugLocalInfo getCurrentLocal(int register) {
-    return generatingMethodSynchronization ? null : state.getLocalInfoForRegister(register);
+  public DebugLocalInfo getIncomingLocal(int register) {
+    return generatingMethodSynchronization ? null : state.getIncomingLocalInfoForRegister(register);
+  }
+
+  @Override
+  public DebugLocalInfo getOutgoingLocal(int register) {
+    return generatingMethodSynchronization ? null : state.getOutgoingLocalInfoForRegister(register);
   }
 
   @Override
@@ -1740,14 +1762,7 @@
   }
 
   private void updateState(LabelNode insn) {
-    int offset = getOffset(insn);
-    // Close scope of locals ending at this point.
-    List<Local> locals = state.getLocalsToClose(offset);
-    state.closeLocals(locals);
-    // Open the scope of locals starting at this point.
-    if (insn != initialLabel) {
-      state.openLocals(offset);
-    }
+    // Intentionally empty.
   }
 
   private void updateState(LdcInsnNode insn) {
@@ -2455,8 +2470,9 @@
     } else {
       assert Opcodes.ISTORE <= opcode && opcode <= Opcodes.ASTORE;
       Slot src = state.pop(expectedType);
-      int dest = state.writeLocal(insn.var, src.type);
+      int dest = state.getLocalRegister(insn.var, src.type);
       builder.addMove(valueType(src.type), dest, src.register);
+      state.writeLocal(insn.var, src.type);
     }
   }
 
@@ -2697,20 +2713,7 @@
   }
 
   private void build(LabelNode insn, IRBuilder builder) {
-    int offset = getOffset(insn);
-    // Close locals starting at this point.
-    List<Local> locals = state.getLocalsToClose(offset);
-    for (Local local : locals) {
-      builder.addDebugLocalEnd(local.slot.register, local.info);
-    }
-    state.closeLocals(locals);
-
-    // Open the scope of locals starting at this point.
-    if (insn != initialLabel) {
-      for (Local local : state.openLocals(offset)) {
-        builder.addDebugLocalStart(local.slot.register, local.info);
-      }
-    }
+    // Intentionally empty.
   }
 
   private void build(LdcInsnNode insn, IRBuilder builder) throws ApiLevelException {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index af09854..99bca1c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.JarApplicationReader;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.Pair;
 import com.google.common.base.Equivalence;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
@@ -426,41 +427,108 @@
 
   // Local variable procedures.
 
-  public List<Local> openLocals(int offset) {
-    LocalsAtOffset localsAtOffset = localsAtOffsetTable.get(offset);
-    if (localsAtOffset == null) {
-      return Collections.emptyList();
+  private List<Pair<Integer, Type>> writes = new ArrayList<>();
+  private List<Local> localsToOpen = new ArrayList<>();
+  private List<Local> localsToClose = new ArrayList<>();
+
+  public void beginTransaction(int offset, boolean hasNextInstruction) {
+    getLocalsToClose(offset);
+    if (hasNextInstruction) {
+      getLocalsToOpen(offset);
+    } else {
+      assert localsToOpen.isEmpty();
+      localsToOpen.clear();
     }
-    ArrayList<Local> locals = new ArrayList<>(localsAtOffset.starts.size());
-    for (LocalNodeInfo start : localsAtOffset.starts) {
-      locals.add(setLocalInfo(start.node.index, start.type, start.info));
-    }
-    return locals;
+    assert writes.isEmpty();
+    writes.clear();
   }
 
-  public List<Local> getLocalsToClose(int offset) {
+  public void beginTransactionSynthetic() {
+    assert localsToClose.isEmpty();
+    assert localsToOpen.isEmpty();
+    assert writes.isEmpty();
+    writes.clear();
+  }
+
+  public void endTransaction() {
+    closeLocals();
+    applyWrites();
+    openLocals();
+  }
+
+  public void beginTransactionAtBlockStart(int offset) {
+    // If there are locals closing at the start of a block, just ignore them,
+    // since we should have closed them at the end of the predecessor blocks.
+    assert localsToClose.isEmpty();
+    assert writes.isEmpty();
+    getLocalsToOpen(offset);
+  }
+
+  private void applyWrites() {
+    for (Pair<Integer, Type> write : writes) {
+      applyWriteLocal(write.getFirst(), write.getSecond());
+    }
+    writes.clear();
+  }
+
+  private void getLocalsToOpen(int offset) {
+    assert localsToOpen.isEmpty();
     LocalsAtOffset localsAtOffset = localsAtOffsetTable.get(offset);
     if (localsAtOffset == null) {
-      return Collections.emptyList();
+      return;
     }
-    ArrayList<Local> locals = new ArrayList<>(localsAtOffset.ends.size());
+    for (LocalNodeInfo start : localsAtOffset.starts) {
+      int register = getLocalRegister(start.node.index, start.type);
+      Local existingLocal = getLocalForRegister(register);
+      assert existingLocal != null;
+      Local local = new Local(existingLocal.slot, start.info);
+      localsToOpen.add(local);
+    }
+  }
+
+  private void openLocals() {
+    for (Local local : localsToOpen) {
+      assert local != null;
+      openLocal(local);
+    }
+    localsToOpen.clear();
+  }
+
+  private void getLocalsToClose(int offset) {
+    assert localsToClose.isEmpty();
+    LocalsAtOffset localsAtOffset = localsAtOffsetTable.get(offset);
+    if (localsAtOffset == null) {
+      return;
+    }
     for (LocalNodeInfo end : localsAtOffset.ends) {
       int register = getLocalRegister(end.node.index, end.type);
       Local local = getLocalForRegister(register);
       assert local != null;
       if (local.info != null) {
-        locals.add(local);
+        localsToClose.add(local);
       }
     }
-    return locals;
   }
 
-  public void closeLocals(List<Local> localsToClose) {
-    for (Local local : localsToClose) {
-      assert local != null;
-      assert local == getLocalForRegister(local.slot.register);
+  private void closeLocals() {
+    for (Local localToClose : localsToClose) {
+      assert localToClose != null;
+      // Since the instruction preceding this point may have strongly updated the type,
+      // and the localsToClose list may have been generated before the preceding instruction,
+      // we cannot assert that localToClose == local at this point.
+      // We only set the info to null and leave the type as-is.
+      Local local = getLocalForRegister(localToClose.slot.register);
       setLocalForRegister(local.slot.register, local.slot.type, null);
     }
+    localsToClose.clear();
+  }
+
+  public List<Local> getLocalsToClose() {
+    return localsToClose;
+  }
+
+  public List<Local> getLocalsToOpen() {
+    return localsToOpen;
   }
 
   public ImmutableList<Local> getLocals() {
@@ -485,7 +553,7 @@
     return Slot.isCategory1(type) ? index + localsSize : index + 2 * localsSize;
   }
 
-  public DebugLocalInfo getLocalInfoForRegister(int register) {
+  public DebugLocalInfo getIncomingLocalInfoForRegister(int register) {
     if (register >= locals.length) {
       return null;
     }
@@ -493,6 +561,27 @@
     return local == null ? null : local.info;
   }
 
+  public DebugLocalInfo getOutgoingLocalInfoForRegister(int register) {
+    DebugLocalInfo local = getIncomingLocalInfoForRegister(register);
+    if (local != null && localsToClose != null) {
+      for (Local localToClose : localsToClose) {
+        if (localToClose.slot.register == register) {
+          local = null;
+          break;
+        }
+      }
+    }
+    if (local == null && localsToOpen != null) {
+      for (Local localToOpen : localsToOpen) {
+        if (localToOpen.slot.register == register) {
+          local = localToOpen.info;
+          break;
+        }
+      }
+    }
+    return local;
+  }
+
   private Local getLocalForRegister(int register) {
     return locals[register];
   }
@@ -512,24 +601,25 @@
     return local;
   }
 
-  private Local setLocalInfo(int index, Type type, DebugLocalInfo info) {
-    return setLocalInfoForRegister(getLocalRegister(index, type), info);
-  }
-
-  private Local setLocalInfoForRegister(int register, DebugLocalInfo info) {
-    Local existingLocal = getLocalForRegister(register);
+  private void openLocal(Local localToOpen) {
+    int register = localToOpen.slot.register;
+    DebugLocalInfo info = localToOpen.info;
+    Local local = getLocalForRegister(register);
     Type type = Type.getType(info.type.toDescriptorString());
-    if (!existingLocal.slot.isCompatibleWith(type)) {
+    if (!local.slot.isCompatibleWith(type)) {
       throw new InvalidDebugInfoException(
-          "Attempt to define local of type " + prettyType(existingLocal.slot.type) + " as " + info);
+          "Attempt to define local of type " + prettyType(local.slot.type) + " as " + info);
     }
-    Local local = new Local(existingLocal.slot, info);
-    locals[register] = local;
-    return local;
+    // Only update local info; keep slot type intact.
+    locals[register] = new Local(local.slot, localToOpen.info);
   }
 
-
   public int writeLocal(int index, Type type) {
+    writes.add(new Pair<>(index, type));
+    return getLocalRegister(index, type);
+  }
+
+  private void applyWriteLocal(int index, Type type) {
     assert nonNullType(type);
     Local local = getLocal(index, type);
     if (local != null && local.info != null && !local.slot.isCompatibleWith(type)) {
@@ -540,9 +630,8 @@
     // scopes of locals. We assume the program to be verified and overwrite if the types mismatch.
     if (local == null || !typeEquals(local.slot.type, type)) {
       DebugLocalInfo info = local == null ? null : local.info;
-      local = setLocal(index, type, info);
+      setLocal(index, type, info);
     }
-    return local.slot.register;
   }
 
   public boolean typeEquals(Type type1, Type type2) {
@@ -599,7 +688,8 @@
 
   public Slot pop(Type type) {
     Slot slot = pop();
-    assert slot.isCompatibleWith(type);
+    assert slot.isCompatibleWith(type)
+        : "Tried to pop " + prettyType(slot.type) + " as " + prettyType(type);
     return slot;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
index be1c670..cd8efa7 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/SourceCode.java
@@ -22,7 +22,9 @@
   int instructionIndex(int instructionOffset);
   int instructionOffset(int instructionIndex);
 
-  DebugLocalInfo getCurrentLocal(int register);
+  DebugLocalInfo getIncomingLocal(int register);
+
+  DebugLocalInfo getOutgoingLocal(int register);
 
   Position getCurrentPosition();
 
@@ -39,8 +41,6 @@
    */
   int traceInstruction(int instructionIndex, IRBuilder builder);
 
-  void closingCurrentBlockWithFallthrough(int fallthroughInstructionIndex, IRBuilder builder);
-
   // Setup and release resources used temporarily during trace/build.
   void setUp();
   void clear();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index 8fca809..5ea5d0e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -113,8 +113,10 @@
 
     DexItemFactory factory = factory();
     if (a.isArrayType()) {
-      // Arrays are only adaptable to java.lang.Object.
-      return b == factory.objectType;
+      // Arrays are only adaptable to java.lang.Object or other arrays, note that we
+      // don't check element type inheritance in the second case since we assume the
+      // input code is verifiable.
+      return b == factory.objectType || b.isArrayType();
     }
 
     if (a.isPrimitiveType()) {
@@ -342,8 +344,10 @@
       }
     }
 
-    if (fromType.isArrayType() && toType == factory().objectType) {
+    if (fromType.isArrayType() && (toType == factory().objectType || toType.isArrayType())) {
       // If `fromType` is an array and `toType` is java.lang.Object, no cast is needed.
+      // If both `fromType` and `toType` are arrays, no cast is needed since we assume
+      // the input code is verifiable.
       return register;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 16050b2..85a3ac6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -16,6 +17,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue.DexValueBoolean;
 import com.android.tools.r8.graph.DexValue.DexValueByte;
@@ -1084,47 +1086,47 @@
           if (put.inValue().isConstant()) {
             if (put.inValue().isConstNumber()) {
               assert put.inValue().isZero();
-              encodedField.staticValue = DexValueNull.NULL;
+              encodedField.setStaticValue(DexValueNull.NULL);
             } else {
               ConstString cnst = put.inValue().getConstInstruction().asConstString();
-              encodedField.staticValue = new DexValueString(cnst.getValue());
+              encodedField.setStaticValue(new DexValueString(cnst.getValue()));
             }
           } else {
             InvokeVirtual invoke = put.inValue().definition.asInvokeVirtual();
             String name = method.method.getHolder().toSourceString();
             if (invoke.getInvokedMethod() == dexItemFactory.classMethods.getSimpleName) {
               String simpleName = name.substring(name.lastIndexOf('.') + 1);
-              encodedField.staticValue =
-                  new DexValueString(dexItemFactory.createString(simpleName));
+              encodedField.setStaticValue(
+                  new DexValueString(dexItemFactory.createString(simpleName)));
             } else {
               assert invoke.getInvokedMethod() == dexItemFactory.classMethods.getName;
-              encodedField.staticValue = new DexValueString(dexItemFactory.createString(name));
+              encodedField.setStaticValue(new DexValueString(dexItemFactory.createString(name)));
             }
           }
         } else if (field.type.isClassType() || field.type.isArrayType()) {
           if (put.inValue().isZero()) {
-            encodedField.staticValue = DexValueNull.NULL;
+            encodedField.setStaticValue(DexValueNull.NULL);
           } else {
             throw new Unreachable("Unexpected default value for field type " + field.type + ".");
           }
         } else {
           ConstNumber cnst = put.inValue().getConstInstruction().asConstNumber();
           if (field.type == dexItemFactory.booleanType) {
-            encodedField.staticValue = DexValueBoolean.create(cnst.getBooleanValue());
+            encodedField.setStaticValue(DexValueBoolean.create(cnst.getBooleanValue()));
           } else if (field.type == dexItemFactory.byteType) {
-            encodedField.staticValue = DexValueByte.create((byte) cnst.getIntValue());
+            encodedField.setStaticValue(DexValueByte.create((byte) cnst.getIntValue()));
           } else if (field.type == dexItemFactory.shortType) {
-            encodedField.staticValue = DexValueShort.create((short) cnst.getIntValue());
+            encodedField.setStaticValue(DexValueShort.create((short) cnst.getIntValue()));
           } else if (field.type == dexItemFactory.intType) {
-            encodedField.staticValue = DexValueInt.create(cnst.getIntValue());
+            encodedField.setStaticValue(DexValueInt.create(cnst.getIntValue()));
           } else if (field.type == dexItemFactory.longType) {
-            encodedField.staticValue = DexValueLong.create(cnst.getLongValue());
+            encodedField.setStaticValue(DexValueLong.create(cnst.getLongValue()));
           } else if (field.type == dexItemFactory.floatType) {
-            encodedField.staticValue = DexValueFloat.create(cnst.getFloatValue());
+            encodedField.setStaticValue(DexValueFloat.create(cnst.getFloatValue()));
           } else if (field.type == dexItemFactory.doubleType) {
-            encodedField.staticValue = DexValueDouble.create(cnst.getDoubleValue());
+            encodedField.setStaticValue(DexValueDouble.create(cnst.getDoubleValue()));
           } else if (field.type == dexItemFactory.charType) {
-            encodedField.staticValue = DexValueChar.create((char) cnst.getIntValue());
+            encodedField.setStaticValue(DexValueChar.create((char) cnst.getIntValue()));
           } else {
             throw new Unreachable("Unexpected field type " + field.type + ".");
           }
@@ -1206,8 +1208,7 @@
           && outValue.isUsed()
           && outValue.numberOfPhiUsers() == 0
           && outValue.uniqueUsers().stream().allMatch(isCheckcastToSubtype)) {
-        outValue.replaceUsers(inValue);
-        it.removeOrReplaceByDebugLocalRead();
+        removeOrReplaceByDebugLocalWrite(it, inValue, outValue);
         continue;
       }
 
@@ -1232,8 +1233,7 @@
         if (TypeLatticeElement.lessThanOrEqual(appInfo, inTypeLattice, castTypeLattice)) {
           assert outTypeLattice.equals(inTypeLattice);
           needToRemoveTrivialPhis = needToRemoveTrivialPhis || outValue.numberOfPhiUsers() != 0;
-          outValue.replaceUsers(inValue);
-          it.removeOrReplaceByDebugLocalRead();
+          removeOrReplaceByDebugLocalWrite(it, inValue, outValue);
           continue;
         }
         // Otherwise, keep the checkcast to preserve verification errors. E.g., down-cast:
@@ -1253,9 +1253,21 @@
     if (needToRemoveTrivialPhis) {
       code.removeAllTrivialPhis();
     }
+    it = code.instructionIterator();
     assert code.isConsistentSSA();
   }
 
+  private void removeOrReplaceByDebugLocalWrite(
+      InstructionIterator it, Value inValue, Value outValue) {
+    if (outValue.getLocalInfo() != inValue.getLocalInfo() && outValue.hasLocalInfo()) {
+      DebugLocalWrite debugLocalWrite = new DebugLocalWrite(outValue, inValue);
+      it.replaceCurrentInstruction(debugLocalWrite);
+    } else {
+      outValue.replaceUsers(inValue);
+      it.removeOrReplaceByDebugLocalRead();
+    }
+  }
+
   private boolean canBeFolded(Instruction instruction) {
     return (instruction.isBinop() && instruction.asBinop().canBeFolded()) ||
         (instruction.isUnop() && instruction.asUnop().canBeFolded());
@@ -1754,7 +1766,8 @@
   }
 
   // TODO(mikaelpeltier) Manage that from and to instruction do not belong to the same block.
-  private static boolean hasLineChangeBetween(Instruction from, Instruction to) {
+  private static boolean hasLocalOrLineChangeBetween(
+      Instruction from, Instruction to, DexString localVar) {
     if (from.getBlock() != to.getBlock()) {
       return true;
     }
@@ -1778,6 +1791,11 @@
       if (instruction == to) {
         return false;
       }
+      if (instruction.outValue() != null && instruction.outValue().hasLocalInfo()) {
+        if (instruction.outValue().getLocalInfo().name == localVar) {
+          return true;
+        }
+      }
     }
     throw new Unreachable();
   }
@@ -1793,21 +1811,29 @@
         }
       }
 
-      InstructionIterator iterator = block.iterator();
+      InstructionListIterator iterator = block.listIterator();
       while (iterator.hasNext()) {
+        Instruction prevInstruction = iterator.peekPrevious();
         Instruction instruction = iterator.next();
         if (instruction.isDebugLocalWrite()) {
           assert instruction.inValues().size() == 1;
           Value inValue = instruction.inValues().get(0);
+          DebugLocalInfo localInfo = instruction.outValue().getLocalInfo();
+          DexString localName = localInfo.name;
           if (!inValue.hasLocalInfo() &&
               inValue.numberOfAllUsers() == 1 &&
               inValue.definition != null &&
-              !hasLineChangeBetween(inValue.definition, instruction)) {
-            inValue.setLocalInfo(instruction.outValue().getLocalInfo());
-            instruction.moveDebugValues(inValue.definition);
+              !hasLocalOrLineChangeBetween(inValue.definition, instruction, localName)) {
+            inValue.setLocalInfo(localInfo);
             instruction.outValue().replaceUsers(inValue);
-            instruction.clearDebugValues();
-            iterator.remove();
+            Value overwrittenLocal = instruction.removeDebugValue(localInfo);
+            if (overwrittenLocal != null) {
+              inValue.definition.addDebugValue(overwrittenLocal);
+            }
+            if (prevInstruction != null) {
+              instruction.moveDebugValues(prevInstruction);
+            }
+            iterator.removeOrReplaceByDebugLocalRead();
           }
         }
       }
@@ -1832,6 +1858,7 @@
         iterator.removeOrReplaceByDebugLocalRead();
         return;
       }
+      assert next.getLocalInfo().name != write.getLocalInfo().name;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 65afb32..cdfa2b1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -78,7 +78,7 @@
       DexEncodedField staticField = appInfo.lookupStaticTarget(field.clazz, field);
       if (staticField != null) {
         Value value = code.createValue(valueType, instruction.getLocalInfo());
-        replacement = staticField.staticValue.asConstInstruction(false, value);
+        replacement = staticField.getStaticValue().asConstInstruction(false, value);
       } else {
         throw new CompilationError(field.clazz.toSourceString() + "." + field.name.toString() +
             " used in assumevalues rule does not exist.");
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index cd02b19..e9781b4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -821,7 +821,12 @@
     }
 
     @Override
-    public DebugLocalInfo getCurrentLocal(int register) {
+    public DebugLocalInfo getIncomingLocal(int register) {
+      return null;
+    }
+
+    @Override
+    public DebugLocalInfo getOutgoingLocal(int register) {
       return null;
     }
 
@@ -832,11 +837,6 @@
     }
 
     @Override
-    public void closingCurrentBlockWithFallthrough(
-        int fallthroughInstructionIndex, IRBuilder builder) {
-    }
-
-    @Override
     public void setUp() {
     }
 
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 2734ff4..7b3eef0 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
@@ -5,7 +5,7 @@
 
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.ConstInstruction;
+import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
@@ -266,10 +266,9 @@
       // Mapping from register number to const number instructions for this basic block.
       // Used to remove redundant const instructions that reloads the same constant into
       // the same register.
-      Map<Integer, ConstInstruction> registerToConstant = new HashMap<>();
+      Map<Integer, ConstNumber> registerToNumber = new HashMap<>();
       MoveEliminator moveEliminator = new MoveEliminator(allocator);
       ListIterator<Instruction> iterator = block.getInstructions().listIterator();
-      boolean mayNoLongerThrow = false;
       while (iterator.hasNext()) {
         Instruction current = iterator.next();
         if (moveEliminator.shouldBeEliminated(current)) {
@@ -277,30 +276,28 @@
         } else if (current.outValue() != null && current.outValue().needsRegister()) {
           Value outValue = current.outValue();
           int instructionNumber = current.getNumber();
-          if (!outValue.hasLocalInfo() && (current.isConstNumber() || current.isConstString())) {
-            if (constantSpilledAtDefinition(current.asConstInstruction(), allocator)) {
+          if (outValue.isConstant() && current.isConstNumber()) {
+            if (constantSpilledAtDefinition(current.asConstNumber(), allocator)) {
               // Remove constant instructions that are spilled at their definition and are
               // therefore unused.
               iterator.remove();
-              mayNoLongerThrow |= current.instructionTypeCanThrow();
+              continue;
+            }
+            int outRegister = allocator.getRegisterForValue(outValue, instructionNumber);
+            ConstNumber numberInRegister = registerToNumber.get(outRegister);
+            if (numberInRegister != null
+                && numberInRegister.identicalNonValueNonPositionParts(current)) {
+              // This instruction is not needed, the same constant is already in this register.
+              // We don't consider the positions of the two (non-throwing) instructions.
+              iterator.remove();
             } else {
-              int outRegister = allocator.getRegisterForValue(outValue, instructionNumber);
-              ConstInstruction constantInRegister = registerToConstant.get(outRegister);
-              if (constantInRegister != null
-                  && constantInRegister.identicalNonValueNonPositionParts(current)) {
-                // This instruction is not needed, the same constant is already in this register.
-                // We don't consider the positions of the two (non-throwing) instructions.
-                iterator.remove();
-                mayNoLongerThrow |= current.instructionTypeCanThrow();
+              // Insert the current constant in the mapping. Make sure to clobber the second
+              // register if wide and register-1 if that defines a wide value.
+              registerToNumber.put(outRegister, current.asConstNumber());
+              if (current.outType().isWide()) {
+                registerToNumber.remove(outRegister + 1);
               } else {
-                // Insert the current constant in the mapping. Make sure to clobber the second
-                // register if wide and register-1 if that defines a wide value.
-                registerToConstant.put(outRegister, current.asConstNumber());
-                if (current.outType().isWide()) {
-                  registerToConstant.remove(outRegister + 1);
-                } else {
-                  removeWideConstantCovering(registerToConstant, outRegister);
-                }
+                removeWideConstantCovering(registerToNumber, outRegister);
               }
             }
           } else {
@@ -308,40 +305,32 @@
             // from the mapping.
             int outRegister = allocator.getRegisterForValue(outValue, instructionNumber);
             for (int i = 0; i < outValue.requiredRegisters(); i++) {
-              registerToConstant.remove(outRegister + i);
+              registerToNumber.remove(outRegister + i);
             }
             // Check if the first register written is the second part of a wide value. If so
             // the wide value is no longer active.
-            removeWideConstantCovering(registerToConstant, outRegister);
+            removeWideConstantCovering(registerToNumber, outRegister);
           }
         }
       }
-
-      if (mayNoLongerThrow && block.hasCatchHandlers() && !block.canThrow()) {
-        block.clearCatchHandlers();
-      }
     }
   }
 
   private static void removeWideConstantCovering(
-      Map<Integer, ConstInstruction> registerToConstant, int register) {
-    ConstInstruction constant = registerToConstant.get(register - 1);
-    if (constant != null && constant.outType().isWide()) {
-      registerToConstant.remove(register - 1);
+      Map<Integer, ConstNumber> registerToNumber, int register) {
+    ConstNumber number = registerToNumber.get(register - 1);
+    if (number != null && number.outType().isWide()) {
+      registerToNumber.remove(register - 1);
     }
   }
 
   private static boolean constantSpilledAtDefinition(
-      ConstInstruction constInstruction, LinearScanRegisterAllocator allocator) {
-    assert constInstruction.isConstNumber() || constInstruction.isConstString();
-    if (constInstruction.outValue().isFixedRegisterValue()) {
+      ConstNumber constNumber, LinearScanRegisterAllocator allocator) {
+    if (constNumber.outValue().isFixedRegisterValue()) {
       return false;
     }
     LiveIntervals definitionIntervals =
-        constInstruction
-            .outValue()
-            .getLiveIntervals()
-            .getSplitCovering(constInstruction.getNumber());
+        constNumber.outValue().getLiveIntervals().getSplitCovering(constNumber.getNumber());
     return definitionIntervals.isSpilledAndRematerializable(allocator);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index f0f5b36..769c281 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -43,6 +43,8 @@
 import it.unimi.dsi.fastutil.ints.IntArraySet;
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -186,6 +188,9 @@
     // There are no linked values prior to register allocation.
     assert noLinkedValues();
     assert code.isConsistentSSA();
+    if (this.code.method.accessFlags.isBridge() && implementationIsBridge(this.code)) {
+      transformBridgeMethod();
+    }
     computeNeedsRegister();
     insertArgumentMoves();
     ImmutableList<BasicBlock> blocks = computeLivenessInformation();
@@ -553,12 +558,6 @@
     }
   }
 
-  @Override
-  public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
-    return isHighRegister(
-        getRegisterForValue(value, instructionNumber) + value.requiredRegisters() - 1);
-  }
-
   public int highestUsedRegister() {
     return registersUsed() - 1;
   }
@@ -673,7 +672,7 @@
       // const number instructions are for values that can be rematerialized instead of
       // spilled.
       assert instruction.getNumber() == -1;
-      assert instruction.isMove() || instruction.isConstNumber() || instruction.isConstString();
+      assert instruction.isMove() || instruction.isConstNumber();
       assert !instruction.isDebugInstruction();
       return true;
     }
@@ -1928,11 +1927,8 @@
         newActive.add(splitChild);
         // If the constant is split before its first actual use, mark the constant as being
         // spilled. That will allows us to remove it afterwards if it is rematerializable.
-        Value intervalsValue = intervals.getValue();
-        boolean isRematerializableConstantValue =
-            intervalsValue.isConstNumber() || intervalsValue.isConstString();
-        if (isRematerializableConstantValue
-            && intervals.getStart() == intervalsValue.definition.getNumber()
+        if (intervals.getValue().isConstNumber()
+            && intervals.getStart() == intervals.getValue().definition.getNumber()
             && intervals.getUses().size() == 1) {
           intervals.setSpilled(true);
         }
@@ -1942,7 +1938,9 @@
             LiveIntervals splitOfSplit = splitChild.splitBefore(splitChild.getFirstUse());
             splitOfSplit.setRegister(intervals.getRegister());
             inactive.add(splitOfSplit);
-          } else if (isRematerializableConstantValue) {
+          } else if (intervals.getValue().isConstNumber()) {
+            // TODO(ager): Do this for all constants. Currently we only rematerialize const
+            // number and therefore we only do it for numbers at this point.
             splitRangesForSpilledConstant(splitChild, registerNumber);
           } else if (intervals.isArgumentInterval()) {
             splitRangesForSpilledArgument(splitChild);
@@ -1973,7 +1971,6 @@
     // register for as long as possible to avoid further moves.
     assert spilled.isSpilled();
     assert !spilled.getValue().isConstNumber();
-    assert !spilled.getValue().isConstString();
     assert !spilled.isLinked() || spilled.isArgumentInterval();
     boolean isSpillingToArgumentRegister =
         (spilled.isArgumentInterval() || registerNumber < numberOfArgumentRegisters);
@@ -2006,7 +2003,7 @@
     // spill we are running low on registers and this constant should get out of the way
     // as much as possible.
     assert spilled.isSpilled();
-    assert spilled.getValue().isConstNumber() || spilled.getValue().isConstString();
+    assert spilled.getValue().isConstNumber();
     assert !spilled.isLinked() || spilled.isArgumentInterval();
     // Do not split range if constant is reused by one of the eleven following instruction.
     int maxGapSize = 11 * INSTRUCTION_NUMBER_DELTA;
@@ -2332,6 +2329,96 @@
     code.blocks.forEach(BasicBlock::clearUserInfo);
   }
 
+  // Rewrites casts on the form "lhs = (T) rhs" into "(T) rhs" and replaces the uses of lhs by rhs.
+  // This transformation helps to ensure that we do not insert unnecessary moves in bridge methods
+  // with an invoke-range instruction, since all the arguments to the invoke-range instruction will
+  // be original, consecutive arguments of the enclosing method (and importantly, not values that
+  // have been defined by a check-cast instruction).
+  private void transformBridgeMethod() {
+    assert implementationIsBridge(this.code);
+    BasicBlock entry = this.code.blocks.getFirst();
+    InstructionListIterator iterator = entry.listIterator();
+    // Create a mapping from argument values to their index, while scanning over the arguments.
+    Reference2IntMap<Value> argumentIndices = new Reference2IntArrayMap<>();
+    while (iterator.peekNext().isArgument()) {
+      Value argument = iterator.next().asArgument().outValue();
+      argumentIndices.put(argument, argumentIndices.size());
+    }
+    // Move forward until the invocation.
+    while (!iterator.peekNext().isInvoke()) {
+      iterator.next();
+    }
+    Invoke invokeInstruction = iterator.peekNext().asInvoke();
+    // Determine if all of the arguments can be cast without having to move them into lower
+    // registers.
+    int numberOfRequiredRegisters = numberOfArgumentRegisters;
+    if (invokeInstruction.outValue() != null) {
+      numberOfRequiredRegisters += invokeInstruction.outValue().requiredRegisters();
+    }
+    if (numberOfRequiredRegisters - 1 > Constants.U8BIT_MAX) {
+      return;
+    }
+    // Determine if the arguments are consecutive input arguments.
+    List<Value> arguments = invokeInstruction.arguments();
+    if (arguments.size() >= 1) {
+      int previousArgumentIndex = -1;
+      for (int i = 0; i < arguments.size(); ++i) {
+        Value current = arguments.get(i);
+        if (!current.isArgument()) {
+          current = current.definition.asCheckCast().object();
+        }
+        assert current.isArgument();
+        int currentArgumentIndex = argumentIndices.getInt(current);
+        if (previousArgumentIndex >= 0 && currentArgumentIndex != previousArgumentIndex + 1) {
+          return;
+        }
+        previousArgumentIndex = currentArgumentIndex;
+      }
+    } else {
+      return;
+    }
+
+    // Rewrite all casts before the invocation on the form "lhs = (T) rhs" into "(T) rhs", and
+    // replace the uses of lhs by rhs.
+    while (iterator.peekPrevious().isCheckCast()) {
+      CheckCast castInstruction = iterator.previous().asCheckCast();
+      castInstruction.outValue().replaceUsers(castInstruction.object());
+      castInstruction.setOutValue(null);
+    }
+  }
+
+  // Returns true if the IR for this method consists of zero or more arguments, zero or more casts
+  // of the arguments, a single invocation, an optional cast of the result, and a return (in this
+  // particular order).
+  private static boolean implementationIsBridge(IRCode code) {
+    if (code.blocks.size() > 1) {
+      return false;
+    }
+    InstructionListIterator iterator = code.blocks.getFirst().listIterator();
+    // Move forward to the first instruction after the definition of the arguments.
+    while (iterator.hasNext() && iterator.peekNext().isArgument()) {
+      iterator.next();
+    }
+    // Move forward to the first instruction after the casts.
+    while (iterator.hasNext()
+        && iterator.peekNext().isCheckCast()
+        && iterator.peekNext().asCheckCast().object().isArgument()) {
+      iterator.next();
+    }
+    // Check if there is an invoke instruction followed by an optional cast of the result,
+    // and a return.
+    if (!iterator.hasNext() || !iterator.next().isInvoke()) {
+      return false;
+    }
+    if (iterator.hasNext() && iterator.peekNext().isCheckCast()) {
+      iterator.next();
+    }
+    if (!iterator.hasNext() || !iterator.next().isReturn()) {
+      return false;
+    }
+    return true;
+  }
+
   private Value createValue(ValueType type) {
     Value value = code.createValue(type, null);
     value.setNeedsRegister(true);
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
index d7c6588..7e398e6 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
@@ -99,8 +99,8 @@
     if (value.isArgument()) {
       return true;
     }
-    boolean isRematerializableConstantValue = value.isConstNumber() || value.isConstString();
-    if (!isRematerializableConstantValue) {
+    // TODO(ager): rematerialize const string as well.
+    if (!value.isConstNumber()) {
       return false;
     }
     // If one of the non-spilled splits uses a register that is higher than U8BIT_MAX we cannot
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
index 8fcc6b0..f9d0bec 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
@@ -10,7 +10,6 @@
   void allocateRegisters(boolean debug);
   int registersUsed();
   int getRegisterForValue(Value value, int instructionNumber);
-  boolean argumentValueUsesHighRegister(Value value, int instructionNumber);
   int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber);
   InternalOptions getOptions();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
index b1e5e4f..d1fe04c 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.ir.code.Argument;
 import com.android.tools.r8.ir.code.ConstInstruction;
 import com.android.tools.r8.ir.code.ConstNumber;
-import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.FixedRegisterValue;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -141,12 +140,10 @@
         instruction = new Move(to, from);
       } else {
         assert move.definition.isOutConstant();
-        Value to = new FixedRegisterValue(move.definition.outType(), move.dst);
         ConstInstruction definition = move.definition.getOutConstantConstInstruction();
         if (definition.isConstNumber()) {
+          Value to = new FixedRegisterValue(move.definition.outType(), move.dst);
           instruction = new ConstNumber(to, definition.asConstNumber().getRawValue());
-        } else if (definition.isConstString()) {
-          instruction = new ConstString(to, definition.asConstString().getValue());
         } else {
           throw new Unreachable("Unexpected definition");
         }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
index 5a6cc2d..03206b5 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticSourceCode.java
@@ -133,7 +133,12 @@
   }
 
   @Override
-  public DebugLocalInfo getCurrentLocal(int register) {
+  public DebugLocalInfo getIncomingLocal(int register) {
+    return null;
+  }
+
+  @Override
+  public DebugLocalInfo getOutgoingLocal(int register) {
     return null;
   }
 
@@ -144,11 +149,6 @@
   }
 
   @Override
-  public final void closingCurrentBlockWithFallthrough(
-      int fallthroughInstructionIndex, IRBuilder builder) {
-  }
-
-  @Override
   public final void setUp() {
     assert constructors.isEmpty();
     prepareInstructions();
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index a7b3f77..e49ad85 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexAnnotation;
 import com.android.tools.r8.graph.DexAnnotationElement;
 import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexAnnotationSetRefList;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexEncodedAnnotation;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -32,6 +33,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueType;
 import com.android.tools.r8.graph.DexValue.UnknownDexValue;
 import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.JarClassFileReader;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier;
 import com.android.tools.r8.utils.ExceptionUtils;
@@ -217,10 +219,10 @@
   }
 
   private Object getStaticValue(DexEncodedField field) {
-    if (!field.accessFlags.isStatic() || field.staticValue == null) {
+    if (!field.accessFlags.isStatic() || !field.hasExplicitStaticValue()) {
       return null;
     }
-    return field.staticValue.asAsmEncodedObject();
+    return field.getStaticValue().asAsmEncodedObject();
   }
 
   private void writeField(DexEncodedField field, ClassWriter writer) {
@@ -250,18 +252,31 @@
       }
     }
     writeAnnotations(visitor::visitAnnotation, method.annotations.annotations);
-    for (int i = 0; i < method.parameterAnnotations.values.length; i++) {
-      final int iFinal = i;
-      writeAnnotations(
-          (d, vis) -> visitor.visitParameterAnnotation(iFinal, d, vis),
-          method.parameterAnnotations.values[i].annotations);
-    }
+    writeParameterAnnotations(visitor, method.parameterAnnotations);
     if (!method.accessFlags.isAbstract() && !method.accessFlags.isNative()) {
       writeCode(method.getCode(), visitor);
     }
     visitor.visitEnd();
   }
 
+  private void writeParameterAnnotations(
+      MethodVisitor visitor, DexAnnotationSetRefList parameterAnnotations) {
+    int missingParameterAnnotations = parameterAnnotations.getMissingParameterAnnotations();
+    for (int i = 0; i < missingParameterAnnotations; i++) {
+      AnnotationVisitor av =
+          visitor.visitParameterAnnotation(i, JarClassFileReader.SYNTHETIC_ANNOTATION, false);
+      if (av != null) {
+        av.visitEnd();
+      }
+    }
+    for (int i = 0; i < parameterAnnotations.values.length; i++) {
+      int parameterIndex = i + missingParameterAnnotations;
+      writeAnnotations(
+          (d, vis) -> visitor.visitParameterAnnotation(parameterIndex, d, vis),
+          parameterAnnotations.values[i].annotations);
+    }
+  }
+
   private interface AnnotationConsumer {
     AnnotationVisitor visit(String desc, boolean visible);
   }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index f766061..2fd66d0 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardClassFilter;
@@ -62,13 +63,17 @@
   }
 
   private void adaptClassStringsInField(DexEncodedField encodedField) {
-    if (!(encodedField.staticValue instanceof DexValueString)) {
+    if (!encodedField.accessFlags.isStatic()) {
       return;
     }
-    DexString original = ((DexValueString) encodedField.staticValue).getValue();
+    DexValue staticValue = encodedField.getStaticValue();
+    if (!(staticValue instanceof DexValueString)) {
+      return;
+    }
+    DexString original = ((DexValueString) staticValue).getValue();
     DexString renamed = getRenamedStringLiteral(original);
     if (renamed != original) {
-      encodedField.staticValue = new DexValueString(renamed);
+      encodedField.setStaticValue(new DexValueString(renamed));
     }
   }
 
@@ -123,12 +128,16 @@
   }
 
   private void replaceIdentifierNameStringInField(DexEncodedField encodedField) {
-    if (!(encodedField.staticValue instanceof DexValueString)) {
+    if (!encodedField.accessFlags.isStatic()) {
       return;
     }
-    DexString original = ((DexValueString) encodedField.staticValue).getValue();
+    DexValue staticValue = encodedField.getStaticValue();
+    if (!(staticValue instanceof DexValueString)) {
+      return;
+    }
+    DexString original = ((DexValueString) staticValue).getValue();
     if (original instanceof DexItemBasedString) {
-      encodedField.staticValue = new DexValueString(materialize((DexItemBasedString) original));
+      encodedField.setStaticValue(new DexValueString(materialize((DexItemBasedString) original)));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
index f6e087b..57e8514 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstString;
@@ -61,13 +62,17 @@
     if (!identifierNameStrings.contains(encodedField.field)) {
       return;
     }
-    if (!(encodedField.staticValue instanceof DexValueString)) {
+    if (!encodedField.accessFlags.isStatic()) {
       return;
     }
-    DexString original = ((DexValueString) encodedField.staticValue).getValue();
+    DexValue staticValue = encodedField.getStaticValue();
+    if (!(staticValue instanceof DexValueString)) {
+      return;
+    }
+    DexString original = ((DexValueString) staticValue).getValue();
     DexItemBasedString itemBasedString = inferMemberOrTypeFromNameString(appInfo, original);
     if (itemBasedString != null) {
-      encodedField.staticValue = new DexValueString(itemBasedString);
+      encodedField.setStaticValue(new DexValueString(itemBasedString));
     }
   }
 
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 a6a9a11..c68122c 100644
--- a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
@@ -93,14 +93,14 @@
     @Override
     public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
       DexMethod previous = previousLense.lookupMethod(method, context);
-      DexMethod target = bridgeTargetToBridgeMap.get(previous);
+      DexMethod bridge = bridgeTargetToBridgeMap.get(previous);
       // Do not forward calls from a bridge method to itself while the bridge method is still
       // a bridge.
-      if (target == null ||
-          (context.accessFlags.isBridge() && target == context.method)) {
+      if (bridge == null
+          || (context.accessFlags.isBridge() && bridge == context.method)) {
         return previous;
       } else {
-        return target;
+        return bridge;
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
index 7c53ff8..1c969f6 100644
--- a/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
+++ b/src/main/java/com/android/tools/r8/utils/FeatureClassMapping.java
@@ -39,6 +39,7 @@
 public class FeatureClassMapping {
 
   HashMap<String, String> parsedRules = new HashMap<>(); // Already parsed rules.
+  boolean usesOnlyExactMappings = true;
 
   HashSet<FeaturePredicate> mappings = new HashSet<>();
 
@@ -76,6 +77,7 @@
           mapping.addMapping(javaType, featureJar.getOutputName());
       }
     }
+    assert mapping.usesOnlyExactMappings;
     return mapping;
   }
 
@@ -93,19 +95,22 @@
   }
 
   public String featureForClass(String clazz) throws FeatureMappingException {
-    // Todo(ricow): improve performance (e.g., direct lookup of class predicates through hashmap).
-    FeaturePredicate bestMatch = null;
-    for (FeaturePredicate mapping : mappings) {
-      if (mapping.match(clazz)) {
-        if (bestMatch == null || bestMatch.predicate.length() < mapping.predicate.length()) {
-          bestMatch = mapping;
+    if (usesOnlyExactMappings) {
+      return parsedRules.getOrDefault(clazz, baseName);
+    } else {
+      FeaturePredicate bestMatch = null;
+      for (FeaturePredicate mapping : mappings) {
+        if (mapping.match(clazz)) {
+          if (bestMatch == null || bestMatch.predicate.length() < mapping.predicate.length()) {
+            bestMatch = mapping;
+          }
         }
       }
+      if (bestMatch == null) {
+        return baseName;
+      }
+      return bestMatch.feature;
     }
-    if (bestMatch == null) {
-      return baseName;
-    }
-    return bestMatch.feature;
   }
 
   private void parseAndAdd(String line, int lineNumber) throws FeatureMappingException {
@@ -140,6 +145,7 @@
     parsedRules.put(predicate, feature);
     FeaturePredicate featurePredicate = new FeaturePredicate(predicate, feature);
     mappings.add(featurePredicate);
+    usesOnlyExactMappings &= featurePredicate.isExactmapping();
   }
 
   private void error(String error, int line) throws FeatureMappingException {
@@ -168,13 +174,18 @@
       if (isCatchAll) {
         this.predicate = "";
       } else if (isWildcard) {
-        this.predicate = predicate.substring(0, predicate.length() - 2);
+        String packageName = predicate.substring(0, predicate.length() - 2);
+        if (!DescriptorUtils.isValidJavaType(packageName)) {
+          throw new FeatureMappingException(packageName + " is not a valid identifier");
+        }
+        // Prefix of a fully-qualified class name, including a terminating dot.
+        this.predicate = predicate.substring(0, predicate.length() - 1);
       } else {
+        if (!DescriptorUtils.isValidJavaType(predicate)) {
+          throw new FeatureMappingException(predicate + " is not a valid identifier");
+        }
         this.predicate = predicate;
       }
-      if (!DescriptorUtils.isValidJavaType(this.predicate) && !isCatchAll) {
-        throw new FeatureMappingException(this.predicate + " is not a valid identifier");
-      }
       this.feature = feature;
     }
 
@@ -187,5 +198,9 @@
         return className.equals(predicate);
       }
     }
+
+    boolean isExactmapping() {
+      return !isWildcard && !isCatchAll;
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 9fd675e..c5ba5cc 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -30,6 +30,10 @@
 
 public class InternalOptions {
 
+  // Set to true to run compilation in a single thread and without randomly shuffling the input.
+  // This makes life easier when running R8 in a debugger.
+  public static final boolean DETERMINISTIC_DEBUGGING = false;
+
   public enum LineNumberOptimization {
     OFF,
     ON,
@@ -94,7 +98,7 @@
   public boolean enableValuePropagation = true;
 
   // Number of threads to use while processing the dex files.
-  public int numberOfThreads = ThreadUtils.NOT_SPECIFIED;
+  public int numberOfThreads = DETERMINISTIC_DEBUGGING ? 1 : ThreadUtils.NOT_SPECIFIED;
   // Print smali disassembly.
   public boolean useSmaliSyntax = false;
   // Verbose output.
@@ -189,6 +193,10 @@
   public boolean enableMinification = true;
   public boolean disableAssertions = true;
   public boolean debugKeepRules = false;
+  // Read input classes into CfCode format (instead of JarCode).
+  public boolean enableCfFrontend = false;
+  // Don't convert Code objects to IRCode.
+  public boolean skipIR = false;
 
   public boolean debug = false;
   public final TestingOptions testing = new TestingOptions();
diff --git a/src/test/examplesAndroidO/lambdadesugaringnplus/LambdasWithStaticAndDefaultMethods.java b/src/test/examplesAndroidO/lambdadesugaringnplus/LambdasWithStaticAndDefaultMethods.java
index 587d947..46cd555 100644
--- a/src/test/examplesAndroidO/lambdadesugaringnplus/LambdasWithStaticAndDefaultMethods.java
+++ b/src/test/examplesAndroidO/lambdadesugaringnplus/LambdasWithStaticAndDefaultMethods.java
@@ -143,6 +143,92 @@
     }
   }
 
+  static class B78901754 {
+    public static class A {
+      public final String msg;
+
+      public A(String msg) {
+        this.msg = msg;
+      }
+    }
+
+    public static class B extends A {
+      public B(String msg) {
+        super(msg);
+      }
+    }
+
+    public interface IAA {
+      A[] foo(A[] p);
+    }
+
+    public interface IAB {
+      A[] foo(B[] p);
+    }
+
+    public interface IBA {
+      B[] foo(A[] p);
+    }
+
+    public interface IBB {
+      B[] foo(B[] p);
+    }
+
+    public static A[] fooAA(A[] p) {
+      return new A[]{new A("fooAA")};
+    }
+
+    public static A[] fooBA(B[] p) {
+      return new A[]{new A("fooBA")};
+    }
+
+    public static B[] fooAB(A[] p) {
+      return new B[]{new B("fooAB")};
+    }
+
+    public static B[] fooBB(B[] p) {
+      return new B[]{new B("fooBB")};
+    }
+
+    public static void testAA(IAA i) {
+      System.out.println(i.foo(null)[0].msg);
+    }
+
+    public static void testAB(IAB i) {
+      System.out.println(i.foo(null)[0].msg);
+    }
+
+    public static void testBA(IBA i) {
+      System.out.println(i.foo(null)[0].msg);
+    }
+
+    public static void testBB(IBB i) {
+      System.out.println(i.foo(null)[0].msg);
+    }
+
+    public static void test() {
+      testAA(B78901754::fooAA);
+      testAA(B78901754::fooAB);
+      // testAA(B78901754::fooBA); javac error: incompatible types: A[] cannot be converted to B[]
+      // testAA(B78901754::fooBB); javac error: incompatible types: A[] cannot be converted to B[]
+
+      testAB(B78901754::fooAA);
+      testAB(B78901754::fooAB);
+      testAB(B78901754::fooBA);
+      testAB(B78901754::fooBB);
+
+      // testBA(B78901754::fooAA); javac error: A[] cannot be converted to B[]
+      testBA(B78901754::fooAB);
+      // testBA(B78901754::fooBA); javac error: incompatible types: A[] cannot be converted to B[]
+      // testBA(B78901754::fooBB); javac error: incompatible types: A[] cannot be converted to B[]
+
+      // testBB(B78901754::fooAA); javac error: A[] cannot be converted to B[]
+      testBB(B78901754::fooAB);
+      // testBB(B78901754::fooBA); javac error: A[] cannot be converted to B[]
+      testBB(B78901754::fooBB);
+    }
+  }
+
   interface B38257037_I1 {
     default Number getNumber() {
       return new Integer(1);
@@ -420,5 +506,6 @@
     B38308515.test();
     B38302860.test();
     B62168701.test();
+    B78901754.test();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
new file mode 100644
index 0000000..18c3436
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -0,0 +1,240 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static com.google.common.io.ByteStreams.toByteArray;
+import static org.junit.Assert.assertEquals;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.util.ASMifier;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+@RunWith(Parameterized.class)
+public class CfFrontendExamplesTest extends TestBase {
+
+  static final Collection<Object[]> TESTS = Arrays.asList(
+    makeTest("arithmetic.Arithmetic"),
+    makeTest("arrayaccess.ArrayAccess"),
+    makeTest("barray.BArray"),
+    makeTest("bridge.BridgeMethod"),
+    makeTest("cse.CommonSubexpressionElimination"),
+    makeTest("constants.Constants"),
+    makeTest("controlflow.ControlFlow"),
+    makeTest("conversions.Conversions"),
+    makeTest("floating_point_annotations.FloatingPointValuedAnnotationTest"),
+    makeTest("filledarray.FilledArray"),
+    makeTest("hello.Hello"),
+    makeTest("ifstatements.IfStatements"),
+    makeTest("instancevariable.InstanceVariable"),
+    makeTest("instanceofstring.InstanceofString"),
+    makeTest("invoke.Invoke"),
+    makeTest("jumbostring.JumboString"),
+    makeTest("loadconst.LoadConst"),
+    makeTest("loop.UdpServer"),
+    makeTest("newarray.NewArray"),
+    makeTest("regalloc.RegAlloc"),
+    makeTest("returns.Returns"),
+    makeTest("staticfield.StaticField"),
+    makeTest("stringbuilding.StringBuilding"),
+    makeTest("switches.Switches"),
+    makeTest("sync.Sync"),
+    makeTest("throwing.Throwing"),
+    makeTest("trivial.Trivial"),
+    makeTest("trycatch.TryCatch"),
+    makeTest("nestedtrycatches.NestedTryCatches"),
+    makeTest("trycatchmany.TryCatchMany"),
+    makeTest("invokeempty.InvokeEmpty"),
+    makeTest("regress.Regress"),
+    makeTest("regress2.Regress2"),
+    makeTest("regress_37726195.Regress"),
+    makeTest("regress_37658666.Regress", CfFrontendExamplesTest::compareRegress37658666),
+    makeTest("regress_37875803.Regress"),
+    makeTest("regress_37955340.Regress"),
+    makeTest("regress_62300145.Regress"),
+    makeTest("regress_64881691.Regress"),
+    makeTest("regress_65104300.Regress"),
+    makeTest("regress_70703087.Test"),
+    makeTest("regress_70736958.Test"),
+    makeTest("regress_70737019.Test"),
+    makeTest("regress_72361252.Test"),
+    makeTest("memberrebinding2.Memberrebinding"),
+    makeTest("memberrebinding3.Memberrebinding"),
+    makeTest("minification.Minification"),
+    makeTest("enclosingmethod.Main"),
+    makeTest("enclosingmethod_proguarded.Main"),
+    makeTest("interfaceinlining.Main"),
+    makeTest("switchmaps.Switches")
+  );
+
+  private static Object[] makeTest(String className) {
+    return makeTest(className, null);
+  }
+
+  private static Object[] makeTest(String className, BiConsumer<byte[], byte[]> comparator) {
+    return new Object[] {className, comparator};
+  }
+
+  @Parameters(name = "{0}")
+  public static Collection<Object[]> data() {
+    return TESTS;
+  }
+
+  private static void compareRegress37658666(byte[] expectedBytes, byte[] actualBytes) {
+    // javac emits LDC(-0.0f) instead of the shorter FCONST_0 FNEG emitted by CfConstNumber.
+    String ldc = "mv.visitLdcInsn(new Float(\"-0.0\"));";
+    String constNeg = "mv.visitInsn(FCONST_0);\nmv.visitInsn(FNEG);";
+    assertEquals(
+        asmToString(expectedBytes).replace(ldc, constNeg),
+        asmToString(actualBytes));
+  }
+
+  private final Path inputJar;
+  private final BiConsumer<byte[], byte[]> comparator;
+
+  public CfFrontendExamplesTest(String clazz, BiConsumer<byte[], byte[]> comparator) {
+    this.comparator = comparator;
+    String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
+    String suffix = "_debuginfo_all";
+    inputJar = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR, pkg + suffix + JAR_EXTENSION);
+  }
+
+  @Test
+  public void test() throws Exception {
+    Path outputJar = temp.getRoot().toPath().resolve("output.jar");
+    R8Command command =
+        R8Command.builder()
+            .addProgramFiles(inputJar)
+            .setMode(CompilationMode.DEBUG)
+            .setOutput(outputJar, OutputMode.ClassFile)
+            .build();
+    ToolHelper.runR8(
+        command,
+        options -> {
+          options.skipIR = true;
+          options.enableCfFrontend = true;
+        });
+    ArchiveClassFileProvider expected = new ArchiveClassFileProvider(inputJar);
+    ArchiveClassFileProvider actual = new ArchiveClassFileProvider(outputJar);
+    assertEquals(getSortedDescriptorList(expected), getSortedDescriptorList(actual));
+    for (String descriptor : expected.getClassDescriptors()) {
+      byte[] expectedBytes = getClassAsBytes(expected, descriptor);
+      byte[] actualBytes = getClassAsBytes(actual, descriptor);
+      if (comparator != null) {
+        comparator.accept(expectedBytes, actualBytes);
+      } else if (!Arrays.equals(expectedBytes, actualBytes)) {
+        assertEquals(
+            "Class " + descriptor + " differs",
+            asmToString(expectedBytes),
+            asmToString(actualBytes));
+      }
+    }
+  }
+
+  private static List<String> getSortedDescriptorList(ArchiveClassFileProvider inputJar) {
+    ArrayList<String> descriptorList = new ArrayList<>(inputJar.getClassDescriptors());
+    Collections.sort(descriptorList);
+    return descriptorList;
+  }
+
+  private static byte[] getClassAsBytes(ArchiveClassFileProvider inputJar, String descriptor)
+      throws Exception {
+    return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
+  }
+
+  private static String asmToString(byte[] clazz) {
+    StringWriter stringWriter = new StringWriter();
+    printAsm(new PrintWriter(stringWriter), clazz);
+    return stringWriter.toString();
+  }
+
+  private static void printAsm(PrintWriter pw, byte[] clazz) {
+    new ClassReader(clazz).accept(new TraceClassVisitor(null, new ASMifierSorted(), pw), 0);
+  }
+
+  /** Sort methods and fields in the output of ASMifier to make diffing possible. */
+  private static class ASMifierSorted extends ASMifier {
+    private static class Part implements Comparable<Part> {
+
+      private final String key;
+      private final int start;
+      private final int end;
+
+      Part(String key, int start, int end) {
+        this.key = key;
+        this.start = start;
+        this.end = end;
+      }
+
+      @Override
+      public int compareTo(Part part) {
+        int i = key.compareTo(part.key);
+        return i != 0 ? i : Integer.compare(start, part.start);
+      }
+    }
+
+    private final List<Part> parts = new ArrayList<>();
+
+    ASMifierSorted() {
+      super(Opcodes.ASM6, "cw", 0);
+    }
+
+    @Override
+    public ASMifier visitField(
+        int access, String name, String desc, String signature, Object value) {
+      init();
+      int i = text.size();
+      ASMifier res = super.visitField(access, name, desc, signature, value);
+      parts.add(new Part((String) text.get(i), i, text.size()));
+      return res;
+    }
+
+    @Override
+    public ASMifier visitMethod(
+        int access, String name, String desc, String signature, String[] exceptions) {
+      init();
+      int i = text.size();
+      ASMifier res = super.visitMethod(access, name, desc, signature, exceptions);
+      parts.add(new Part((String) text.get(i), i, text.size()));
+      return res;
+    }
+
+    private void init() {
+      if (parts.isEmpty()) {
+        parts.add(new Part("", 0, text.size()));
+      }
+    }
+
+    @Override
+    public void print(PrintWriter pw) {
+      init();
+      int end = parts.get(parts.size() - 1).end;
+      Collections.sort(parts);
+      parts.add(new Part("", end, text.size()));
+      ArrayList<Object> tmp = new ArrayList<>(text);
+      text.clear();
+      for (Part part : parts) {
+        for (int i = part.start; i < part.end; i++) {
+          text.add(tmp.get(i));
+        }
+      }
+      super.print(pw);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
new file mode 100644
index 0000000..5c4a79e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
@@ -0,0 +1,457 @@
+// 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.bridgeremoval;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.code.InvokeVirtualRange;
+import com.android.tools.r8.code.ReturnVoid;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+
+public class B77836766 extends TestBase {
+
+  /**
+   * The below Jasmin code mimics the following Kotlin code:
+   *
+   * package java_pkg;
+   *
+   * public interface Itf1 {
+   *   void foo(String arg);
+   * }
+   *
+   * public interface Itf2 {
+   *   void foo(Integer arg);
+   * }
+   *
+   * package kt_pkg;
+   *
+   * internal abstract class AbsCls<T> {
+   *   void foo(T obj) { ... }
+   * }
+   *
+   * internal class Cls1() : AbsCls<String>(), Itf1
+   *
+   * internal class Cls2() : AbsCls<Integer>(), Itf2
+   *
+   *
+   * kotlinc introduced bridge methods Cls?#foo to AbsCls#foo:
+   *
+   * class Cls1 extends AbsCls implements Itf1 {
+   *   public bridge synthetic void foo(String arg) {
+   *     invoke-virtual Cls1#foo(Object)V
+   *   }
+   * }
+   *
+   * Note that we can't write such code in Java because javac requires Itf?#foo, which are
+   * technically abstract methods, to be explicitly overridden.
+   */
+  @Test
+  public void test_bridgeTargetInBase_differentBridges() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder absCls = jasminBuilder.addClass("AbsCls");
+    absCls.setAccess("public abstract");
+    absCls.addFinalMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "aload_1",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder itf1 = jasminBuilder.addInterface("Itf1");
+    itf1.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V");
+
+    ClassBuilder cls1 = jasminBuilder.addClass("Cls1", absCls.name, itf1.name);
+    // Mimic Kotlin's "internal" class
+    cls1.setAccess("");
+    cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder itf2 = jasminBuilder.addInterface("Itf2");
+    itf2.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
+
+    ClassBuilder cls2 = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
+    // Mimic Kotlin's "internal" class
+    cls2.setAccess("");
+    cls2.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder mainClass = jasminBuilder.addClass("Main");
+    mainClass.addMainMethod(
+        ".limit stack 5",
+        ".limit locals 2",
+        "new " + cls1.name,
+        "dup",
+        "invokespecial " + cls1.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "ldc \"Hello\"",
+        "invokevirtual " + cls1.name + "/foo(Ljava/lang/String;)V",
+        "new " + cls2.name,
+        "dup",
+        "invokespecial " + cls2.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "iconst_0",
+        "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
+        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Integer;)V",
+        "return"
+    );
+
+    final String mainClassName = mainClass.name;
+    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+
+    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject absSubject = inspector.clazz(absCls.name);
+    assertThat(absSubject, isPresent());
+    ClassSubject cls1Subject = inspector.clazz(cls1.name);
+    assertThat(cls1Subject, isPresent());
+    ClassSubject cls2Subject = inspector.clazz(cls2.name);
+    assertThat(cls2Subject, isPresent());
+
+    // Cls1#foo and Cls2#foo should not refer to each other.
+    // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
+
+    MethodSubject fooInCls2 =
+        cls2Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
+    assertThat(fooInCls2, isPresent());
+    DexCode code = fooInCls2.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    InvokeVirtualRange invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(absSubject.getDexClass().type, invoke.getMethod().getHolder());
+
+    MethodSubject fooInCls1 =
+        cls1Subject.method("void", "foo", ImmutableList.of("java.lang.String"));
+    assertThat(fooInCls1, isPresent());
+    code = fooInCls1.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(absSubject.getDexClass().type, invoke.getMethod().getHolder());
+  }
+
+  /**
+   * class Base {
+   *   void foo(Object o) {...}
+   * }
+   * interface ItfInteger {
+   *   void foo(Integer o);
+   * }
+   * class DerivedInteger extends Base implements ItfInteger {
+   *   // Bridge method deferring to Base#foo(Object):
+   *   public bridge synthetic void foo(Integer o) {
+   *     foo((Object) o);
+   *   } }
+   * class DerivedString extends Base {
+   *   // Regular non-bridge method calling Base#foo(Object):
+   *   public void bar(String o) {
+   *     foo(o);
+   *   } }
+   */
+  @Test
+  public void test_bridgeTargetInBase_bridgeAndNonBridge() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder baseCls = jasminBuilder.addClass("Base");
+    baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "aload_1",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder itf = jasminBuilder.addInterface("ItfInteger");
+    itf.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
+
+    ClassBuilder cls1 = jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
+    cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
+    cls2.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder mainClass = jasminBuilder.addClass("Main");
+    mainClass.addMainMethod(
+        ".limit stack 5",
+        ".limit locals 2",
+        "new " + cls1.name,
+        "dup",
+        "invokespecial " + cls1.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "iconst_0",
+        "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
+        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Integer;)V",
+        "new " + cls2.name,
+        "dup",
+        "invokespecial " + cls2.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "ldc \"Bar\"",
+        "invokevirtual " + cls2.name + "/bar(Ljava/lang/String;)V",
+        "return"
+    );
+
+    final String mainClassName = mainClass.name;
+    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+
+    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject baseSubject = inspector.clazz(baseCls.name);
+    assertThat(baseSubject, isPresent());
+    ClassSubject cls1Subject = inspector.clazz(cls1.name);
+    assertThat(cls1Subject, isPresent());
+    ClassSubject cls2Subject = inspector.clazz(cls2.name);
+    assertThat(cls2Subject, isPresent());
+
+    // Cls1#foo and Cls2#bar should refer to Base#foo.
+
+    MethodSubject barInCls2 =
+        cls2Subject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    assertThat(barInCls2, isPresent());
+    DexCode code = barInCls2.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    InvokeVirtualRange invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(baseSubject.getDexClass().type, invoke.getMethod().getHolder());
+
+    MethodSubject fooInCls1 =
+        cls1Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
+    assertThat(fooInCls1, isPresent());
+    code = fooInCls1.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(baseSubject.getDexClass().type, invoke.getMethod().getHolder());
+  }
+
+  /**
+   * class Base {
+   *   protected void foo(Object o) { ... }
+   *   // Bridge method deferring to Base#foo(Object):
+   *   public bridge synthetic void foo(Integer o) {
+   *     foo((Object) o);
+   *   } }
+   * class DerivedString extends Base {
+   *   // Regular non-bridge method calling Base#foo(Object):
+   *   public void bar(String o) {
+   *     foo(o);
+   *   } }
+   */
+  @Test
+  public void test_nonBridgeInSubType() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder baseCls = jasminBuilder.addClass("Base");
+    baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "aload_1",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
+        "return");
+    baseCls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + baseCls.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder subCls = jasminBuilder.addClass("DerivedString", baseCls.name);
+    subCls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + subCls.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder mainClass = jasminBuilder.addClass("Main");
+    mainClass.addMainMethod(
+        ".limit stack 5",
+        ".limit locals 2",
+        "new " + baseCls.name,
+        "dup",
+        "invokespecial " + baseCls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "iconst_0",
+        "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
+        "invokevirtual " + baseCls.name + "/foo(Ljava/lang/Integer;)V",
+        "new " + subCls.name,
+        "dup",
+        "invokespecial " + subCls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "ldc \"Bar\"",
+        "invokevirtual " + subCls.name + "/bar(Ljava/lang/String;)V",
+        "return"
+    );
+
+    final String mainClassName = mainClass.name;
+    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+
+    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject baseSubject = inspector.clazz(baseCls.name);
+    assertThat(baseSubject, isPresent());
+    ClassSubject subSubject = inspector.clazz(subCls.name);
+    assertThat(subSubject, isPresent());
+
+    // DerivedString2#bar should refer to Base#foo.
+
+    MethodSubject barInSub =
+        subSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    assertThat(barInSub, isPresent());
+    DexCode code = barInSub.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    InvokeVirtualRange invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(baseSubject.getDexClass().type, invoke.getMethod().getHolder());
+  }
+
+  /*
+   * public class Base {
+   *  public bridge void foo(Integer i) { foo((Object) i); }
+   *  public foo(Object o) { print(o); }
+   *  public bar(String s) { foo(s); }
+   * }
+   */
+  @Test
+  public void test_bridgeTargetInsideTheSameClass() throws Exception {
+    JasminBuilder jasminBuilder = new JasminBuilder();
+
+    ClassBuilder cls = jasminBuilder.addClass("Base");
+    cls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+    cls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
+        ".limit stack 3",
+        ".limit locals 2",
+        "getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "aload_1",
+        "invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
+        "return");
+    cls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
+        ".limit stack 2",
+        ".limit locals 2",
+        "aload_0",
+        "aload_1",
+        "invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
+        "return");
+
+    ClassBuilder mainClass = jasminBuilder.addClass("Main");
+    mainClass.addMainMethod(
+        ".limit stack 5",
+        ".limit locals 2",
+        "new " + cls.name,
+        "dup",
+        "invokespecial " + cls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "iconst_0",
+        "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
+        "invokevirtual " + cls.name + "/foo(Ljava/lang/Integer;)V",
+        "new " + cls.name,
+        "dup",
+        "invokespecial " + cls.name + "/<init>()V",
+        "astore_0",
+        "aload_0",
+        "ldc \"Bar\"",
+        "invokevirtual " + cls.name + "/bar(Ljava/lang/String;)V",
+        "return"
+    );
+    final String mainClassName = mainClass.name;
+    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+
+    DexInspector inspector = new DexInspector(processedApp);
+    ClassSubject baseSubject = inspector.clazz(cls.name);
+    assertThat(baseSubject, isPresent());
+
+    // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
+
+    MethodSubject barInSub =
+        baseSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
+    assertThat(barInSub, isPresent());
+    DexCode code = barInSub.getMethod().getCode().asDexCode();
+    checkInstructions(code, ImmutableList.of(
+        InvokeVirtualRange.class,
+        ReturnVoid.class));
+    InvokeVirtualRange invoke = (InvokeVirtualRange) code.instructions[0];
+    assertEquals(baseSubject.getDexClass().type, invoke.getMethod().getHolder());
+  }
+
+  private AndroidApp runAndVerifyOnJvmAndArt(
+      JasminBuilder jasminBuilder, String mainClassName, String proguardConfig) throws Exception {
+    // Run input program on java.
+    Path outputDirectory = temp.newFolder().toPath();
+    jasminBuilder.writeClassFiles(outputDirectory);
+    ProcessResult javaResult = ToolHelper.runJava(outputDirectory, mainClassName);
+    assertEquals(0, javaResult.exitCode);
+
+    AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig,
+        // Disable inlining to avoid the (short) tested method from being inlined then removed.
+        internalOptions -> internalOptions.enableInlining = false);
+
+    // Run processed (output) program on ART
+    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
+    assertEquals(javaResult.stdout, artResult.stdout);
+    assertEquals(-1, artResult.stderr.indexOf("VerifyError"));
+
+    return processedApp;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/DebugInfoTest.java b/src/test/java/com/android/tools/r8/cf/DebugInfoTest.java
index 6e009e8..04b88e6 100644
--- a/src/test/java/com/android/tools/r8/cf/DebugInfoTest.java
+++ b/src/test/java/com/android/tools/r8/cf/DebugInfoTest.java
@@ -18,8 +18,7 @@
     int intVar;
     if (arg) {
       float floatVar1 = 0f;
-      intVar = (int) floatVar1;
-    } else {
+      intVar = (int) floatVar1; /* No line break before 'else' to avoid DebugPosition */ } else {
       float floatVar2 = 0f;
       intVar = (int) floatVar2;
     }
diff --git a/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
index d371243..45e3c96 100644
--- a/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ContinuousSteppingTest.java
@@ -4,36 +4,195 @@
 
 package com.android.tools.r8.debug;
 
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.Pair;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.stream.Collectors;
 import org.apache.harmony.jpda.tests.framework.jdwp.Value;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 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 ContinuousSteppingTest extends DebugTestBase {
 
-  private static DebugTestConfig javaD8Config;
-  private static DebugTestConfig kotlinD8Config;
+  private static final String MAIN_METHOD_NAME = "main";
+
+  // A list of self-contained jars to process (which do not depend on other jar files).
+  private static final List<Pair<Path, Predicate<Version>>> LIST_OF_JARS = new ConfigListBuilder()
+      .add(DebugTestBase.DEBUGGEE_JAR, ContinuousSteppingTest::allVersions)
+      .add(DebugTestBase.DEBUGGEE_JAVA8_JAR, ContinuousSteppingTest::allVersions)
+      .add(KotlinD8Config.DEBUGGEE_KOTLIN_JAR, ContinuousSteppingTest::allVersions)
+      .addAll(findAllJarsIn(Paths.get(ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR)),
+          ContinuousSteppingTest::fromAndroidN)
+      .addAll(findAllJarsIn(Paths.get(ToolHelper.EXAMPLES_ANDROID_O_BUILD_DIR)),
+          ContinuousSteppingTest::fromAndroidO)
+      .build();
+
+  private static final Map<Path, DebugTestConfig> compiledJarConfig = new HashMap<>();
+
+  private final String mainClass;
+  private final Path jarPath;
+
+  private static class ConfigListBuilder {
+
+    private final Builder<Pair<Path, Predicate<Version>>> builder = ImmutableList.builder();
+
+    public ConfigListBuilder add(Path path, Predicate<Version> predicate) {
+      builder.add(new Pair<>(path, predicate));
+      return this;
+    }
+
+    public ConfigListBuilder addAll(List<Path> paths, Predicate<Version> predicate) {
+      for (Path path : paths) {
+        add(path, predicate);
+      }
+      return this;
+    }
+
+    public List<Pair<Path, Predicate<Version>>> build() {
+      return builder.build();
+    }
+  }
+
+  public static boolean allVersions(Version dexVmVersion) {
+    return true;
+  }
+
+  public static boolean fromAndroidN(Version dexVmVersion) {
+    return dexVmVersion.isAtLeast(Version.V7_0_0);
+  }
+
+  public static boolean fromAndroidO(Version dexVmVersion) {
+    return dexVmVersion.isAtLeast(Version.DEFAULT);
+  }
+
+  private static List<Path> findAllJarsIn(Path root) {
+    try {
+      return Files.walk(root)
+          .filter(p -> p.toFile().getPath().endsWith(FileUtils.JAR_EXTENSION))
+          .collect(Collectors.toList());
+    } catch (IOException e) {
+      return Collections.emptyList();
+    }
+  }
 
   @BeforeClass
   public static void setup() {
-    javaD8Config = new D8DebugTestResourcesConfig(temp);
-    kotlinD8Config = new KotlinD8Config(temp);
+    LIST_OF_JARS.forEach(pair -> {
+      if (pair.getSecond().test(ToolHelper.getDexVm().getVersion())) {
+        Path jarPath = pair.getFirst();
+        DebugTestConfig config = new D8DebugTestConfig().compileAndAdd(temp, jarPath);
+        compiledJarConfig.put(jarPath, config);
+      }
+    });
+  }
+
+  @Parameters(name = "{0} from {1}")
+  public static Collection<Object[]> getData() throws IOException {
+    List<Object[]> testCases = new ArrayList<>();
+    for (Pair<Path, Predicate<Version>> pair : LIST_OF_JARS) {
+      if (pair.getSecond().test(ToolHelper.getDexVm().getVersion())) {
+        Path jarPath = pair.getFirst();
+        List<String> mainClasses = getAllMainClassesFromJar(jarPath);
+        for (String className : mainClasses) {
+          testCases.add(new Object[]{className, jarPath});
+        }
+      }
+    }
+    return testCases;
+  }
+
+  public ContinuousSteppingTest(String mainClass, Path jarPath) {
+    this.mainClass = mainClass;
+    this.jarPath = jarPath;
   }
 
   @Test
-  public void testArithmetic() throws Throwable {
-    runContinuousTest("Arithmetic", javaD8Config);
+  public void testContinuousSingleStep() throws Throwable {
+    assert compiledJarConfig.containsKey(jarPath);
+    DebugTestConfig config = compiledJarConfig.get(jarPath);
+    assert config != null;
+    runContinuousTest(mainClass, config);
   }
 
-  @Test
-  public void testLocals() throws Throwable {
-    runContinuousTest("Locals", javaD8Config);
+  // Returns a list of classes with a "public static void main(String[])" method in the given jar
+  // file.
+  private static List<String> getAllMainClassesFromJar(Path pathToJar) throws IOException {
+    JarInputStream jarInputStream = new JarInputStream(Files.newInputStream(pathToJar,
+        StandardOpenOption.READ));
+    final URL url = pathToJar.toUri().toURL();
+    assert pathToJar.toFile().exists();
+    assert pathToJar.toFile().isFile();
+    List<String> mainClasses = new ArrayList<>();
+    ClassLoader loader = new URLClassLoader(new URL[]{url},
+        Thread.currentThread().getContextClassLoader());
+
+    try {
+      JarEntry entry;
+      while ((entry = jarInputStream.getNextJarEntry()) != null) {
+        String entryName = entry.getName();
+        if (entryName.endsWith(FileUtils.CLASS_EXTENSION)) {
+          String className =
+              entryName.substring(0, entryName.length() - FileUtils.CLASS_EXTENSION.length());
+          className = className.replace(DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR,
+              DescriptorUtils.JAVA_PACKAGE_SEPARATOR);
+          try {
+            Class<?> cls = loader.loadClass(className);
+            if (cls != null) {
+              long mainMethodsCount = Arrays.stream(cls.getMethods())
+                  .filter(ContinuousSteppingTest::isMainMethod)
+                  .count();
+              if (mainMethodsCount == 1) {
+                // Add class to the list
+                mainClasses.add(className);
+              }
+            }
+          } catch (Throwable e) {
+            System.out.println(
+                "Could not load class " + className + " from " + pathToJar.toFile().getPath());
+            return Collections.emptyList();
+          }
+        }
+      }
+    } finally {
+      jarInputStream.close();
+    }
+    return mainClasses;
   }
 
-  @Test
-  public void testKotlinInline() throws Throwable {
-    runContinuousTest("KotlinInline", kotlinD8Config);
+  private static boolean isMainMethod(Method m) {
+    return Modifier.isStatic(m.getModifiers())
+        && m.getReturnType() == void.class
+        && m.getName().equals(MAIN_METHOD_NAME)
+        && m.getParameterCount() == 1
+        && m.getParameterTypes()[0] == String[].class;
   }
 
   private void runContinuousTest(String debuggeeClassName, DebugTestConfig config)
@@ -41,7 +200,7 @@
     runDebugTest(
         config,
         debuggeeClassName,
-        breakpoint(debuggeeClassName, "main"),
+        breakpoint(debuggeeClassName, MAIN_METHOD_NAME),
         run(),
         stepUntil(StepKind.OVER, StepLevel.INSTRUCTION, debuggeeState -> {
           // Fetch local variables.
diff --git a/src/test/java/com/android/tools/r8/debug/IincDebugTest.java b/src/test/java/com/android/tools/r8/debug/IincDebugTest.java
new file mode 100644
index 0000000..5d65dbb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/IincDebugTest.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.debug;
+
+public class IincDebugTest {
+
+  public static void main(String[] args) {
+    int j;
+    {
+      int i = 1;
+      j = i + 1;
+    }
+    System.out.println(j);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/IincDebugTestDump.java b/src/test/java/com/android/tools/r8/debug/IincDebugTestDump.java
new file mode 100644
index 0000000..8ff3d92
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/IincDebugTestDump.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.debug;
+
+import org.objectweb.asm.*;
+
+public class IincDebugTestDump implements Opcodes {
+
+  public static final String CLASS_NAME = "IincDebugTest";
+  public static final String DESCRIPTOR = "L" + CLASS_NAME + ";";
+
+  public static byte[] dump(int iRegister, int jRegister, boolean useInc) {
+
+    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
+    MethodVisitor mv;
+
+    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, CLASS_NAME, null, "java/lang/Object", null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      mv.visitCode();
+      Label methodStart = new Label();
+      mv.visitLabel(methodStart);
+      mv.visitLineNumber(12, methodStart);
+      mv.visitInsn(ICONST_1);
+      mv.visitVarInsn(ISTORE, iRegister);
+      Label iStart = new Label();
+      mv.visitLabel(iStart);
+      mv.visitLineNumber(13, iStart);
+      if (useInc) {
+        assert iRegister == jRegister;
+        mv.visitIincInsn(iRegister, 1);
+      } else {
+        mv.visitVarInsn(ILOAD, iRegister);
+        mv.visitInsn(ICONST_1);
+        mv.visitInsn(IADD);
+        mv.visitVarInsn(ISTORE, jRegister);
+      }
+      Label iEnd = new Label();
+      mv.visitLabel(iEnd);
+      mv.visitLineNumber(15, iEnd);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitVarInsn(ILOAD, jRegister);
+      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
+      Label l3 = new Label();
+      mv.visitLabel(l3);
+      mv.visitLineNumber(16, l3);
+      mv.visitInsn(RETURN);
+      Label jEnd = new Label();
+      mv.visitLabel(jEnd);
+      mv.visitLocalVariable("i", "I", null, iStart, iEnd, iRegister);
+      mv.visitLocalVariable("args", "[Ljava/lang/String;", null, methodStart, jEnd, 0);
+      mv.visitLocalVariable("j", "I", null, iEnd, jEnd, jRegister);
+      mv.visitMaxs(-1, -1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
new file mode 100644
index 0000000..843a0f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/IincDebugTestRunner.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.debug;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import org.junit.Assume;
+import org.junit.Test;
+
+public class IincDebugTestRunner extends DebugTestBase {
+  @Test
+  public void compareDifferentRegister() throws Exception {
+    compareOutput(IincDebugTestDump.dump(1, 2, false));
+  }
+
+  @Test
+  public void compareLoadStoreSameRegister() throws Exception {
+    compareOutput(IincDebugTestDump.dump(1, 1, false));
+  }
+
+  @Test
+  public void compareIinc() throws Exception {
+    compareOutput(IincDebugTestDump.dump(1, 1, true));
+  }
+
+  @Test
+  public void stepDifferentRegister() throws Exception {
+    stepOutput(IincDebugTestDump.dump(1, 2, false));
+  }
+
+  @Test
+  public void stepLoadStoreSameRegister() throws Exception {
+    stepOutput(IincDebugTestDump.dump(1, 1, false));
+  }
+
+  @Test
+  public void stepIinc() throws Exception {
+    stepOutput(IincDebugTestDump.dump(1, 1, true));
+  }
+
+  private void compareOutput(byte[] clazz) throws Exception {
+    Path inputJar = buildInput(clazz);
+    ProcessResult runInput = ToolHelper.runJava(inputJar, IincDebugTestDump.CLASS_NAME);
+    assertEquals(0, runInput.exitCode);
+    ProcessResult runCf = ToolHelper.runJava(buildCf(inputJar), IincDebugTestDump.CLASS_NAME);
+    assertEquals(0, runCf.exitCode);
+    assertEquals(runInput.toString(), runCf.toString());
+    String runDex =
+        ToolHelper.runArtNoVerificationErrors(
+            buildDex(inputJar).toString(), IincDebugTestDump.CLASS_NAME);
+    assertEquals(runInput.stdout, runDex);
+  }
+
+  private void stepOutput(byte[] clazz) throws Exception {
+    // See verifyStateLocation in DebugTestBase.
+    Assume.assumeTrue(
+        "Streaming on Dalvik DEX runtimes has some unknown interference issue",
+        ToolHelper.getDexVm().getVersion().isAtLeast(Version.V6_0_1));
+    Assume.assumeTrue(
+        "Skipping test "
+            + testName.getMethodName()
+            + " because debug tests are not yet supported on Windows",
+        !ToolHelper.isWindows());
+    Path inputJar = buildInput(clazz);
+    new DebugStreamComparator()
+        .add("Input", streamDebugTest(new CfDebugTestConfig(inputJar)))
+        .add("R8/DEX", streamDebugTest(new DexDebugTestConfig(buildDex(inputJar))))
+        .add("R8/CF", streamDebugTest(new CfDebugTestConfig(buildCf(inputJar))))
+        .compare();
+  }
+
+  private Stream<DebuggeeState> streamDebugTest(DebugTestConfig config) throws Exception {
+    return streamDebugTest(config, IincDebugTestDump.CLASS_NAME, ANDROID_FILTER);
+  }
+
+  private Path buildInput(byte[] clazz) {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    ArchiveConsumer inputJarConsumer = new ArchiveConsumer(inputJar);
+    inputJarConsumer.accept(clazz, IincDebugTestDump.DESCRIPTOR, null);
+    inputJarConsumer.finished(null);
+    return inputJar;
+  }
+
+  private Path buildDex(Path inputJar) throws Exception {
+    Path dexJar = temp.getRoot().toPath().resolve("r8dex.jar");
+    build(inputJar, new DexIndexedConsumer.ArchiveConsumer(dexJar));
+    return dexJar;
+  }
+
+  private Path buildCf(Path inputJar) throws Exception {
+    Path cfJar = temp.getRoot().toPath().resolve("r8cf.jar");
+    build(inputJar, new ArchiveConsumer(cfJar));
+    return cfJar;
+  }
+
+  private void build(Path inputJar, ProgramConsumer consumer) throws Exception {
+    Builder builder =
+        R8Command.builder()
+            .setMode(CompilationMode.DEBUG)
+            .setProgramConsumer(consumer)
+            .addProgramFiles(inputJar);
+    if ((consumer instanceof ClassFileConsumer)) {
+      builder.addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME));
+    } else {
+      builder.addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()));
+    }
+    // TODO(b/75997473): Enable inlining when supported by CF backend
+    ToolHelper.runR8(
+        builder.build(),
+        options -> {
+          options.enableInlining = false;
+          options.invalidDebugInfoFatal = true;
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinD8Config.java b/src/test/java/com/android/tools/r8/debug/KotlinD8Config.java
index fa5f5ca..d828a5e 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinD8Config.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinD8Config.java
@@ -17,7 +17,7 @@
  */
 class KotlinD8Config extends D8DebugTestConfig {
 
-  private static final Path DEBUGGEE_KOTLIN_JAR =
+  public static final Path DEBUGGEE_KOTLIN_JAR =
       Paths.get(ToolHelper.BUILD_DIR, "test", "debug_test_resources_kotlin.jar");
 
   private static AndroidApp compiledResources = null;
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoDump.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoDump.java
new file mode 100644
index 0000000..df03ac3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoDump.java
@@ -0,0 +1,132 @@
+// 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.debuginfo;
+
+import org.objectweb.asm.AnnotationVisitor;
+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;
+
+public class DebugInfoDump implements Opcodes {
+
+  private static final String INTERNAL_NAME = "Foo";
+  public static final String CLASS_NAME = INTERNAL_NAME.replace('/', '.');
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    AnnotationVisitor av0;
+
+    cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, INTERNAL_NAME, null, "java/lang/Object", null);
+
+    {
+      fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "f1", "L" + INTERNAL_NAME + ";", null, null);
+      fv.visitEnd();
+    }
+    {
+      fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "f2", "Ljava/util/List;", null, null);
+      fv.visitEnd();
+    }
+    foo(cw);
+    main(cw);
+    bar(cw);
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private static void bar(ClassWriter cw) {
+    MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "bar", "()I", null, null);
+    mv.visitCode();
+    mv.visitLdcInsn(42);
+    mv.visitInsn(IRETURN);
+    mv.visitMaxs(1, 1);
+    mv.visitEnd();
+  }
+
+  private static void main(ClassWriter cw) {
+    MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+    mv.visitCode();
+    mv.visitInsn(RETURN);
+    mv.visitMaxs(0, 1);
+    mv.visitEnd();
+  }
+
+  private static void foo(ClassWriter cw) {
+    MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "foo", "()I", null, null);
+    Label[] labels = new Label[11];
+    for (int i = 0; i < labels.length; i++) {
+      labels[i] = new Label();
+    }
+    mv.visitCode();
+    mv.visitLabel(labels[0]);
+    mv.visitLineNumber(10, labels[0]);
+    mv.visitIntInsn(BIPUSH, 12);
+    mv.visitVarInsn(ISTORE, 1);
+    mv.visitLabel(labels[1]);
+    mv.visitLineNumber(11, labels[1]);
+    mv.visitIincInsn(1, 28);
+    mv.visitLabel(labels[2]);
+    mv.visitLineNumber(13, labels[2]);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "f1", "L" + INTERNAL_NAME + ";");
+    mv.visitJumpInsn(IFNULL, labels[3]);
+    mv.visitLabel(labels[4]);
+    mv.visitLineNumber(14, labels[4]);
+    mv.visitVarInsn(ILOAD, 1);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "f1", "L" + INTERNAL_NAME + ";");
+    mv.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_NAME, "bar", "()I", false);
+    mv.visitInsn(IADD);
+    mv.visitVarInsn(ISTORE, 1);
+    mv.visitLabel(labels[3]);
+    mv.visitLineNumber(17, labels[3]);
+    mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] {Opcodes.INTEGER}, 0, null);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "f2", "Ljava/util/List;");
+    mv.visitJumpInsn(IFNULL, labels[5]);
+    mv.visitLabel(labels[6]);
+    mv.visitLineNumber(18, labels[6]);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "f2", "Ljava/util/List;");
+    mv.visitMethodInsn(
+        INVOKEINTERFACE, "java/util/List", "iterator", "()Ljava/util/Iterator;", true);
+    mv.visitVarInsn(ASTORE, 2);
+    mv.visitLabel(labels[7]);
+    mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] {"java/util/Iterator"}, 0, null);
+    mv.visitVarInsn(ALOAD, 2);
+    mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true);
+    mv.visitJumpInsn(IFEQ, labels[5]);
+    mv.visitVarInsn(ALOAD, 2);
+    mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true);
+    mv.visitTypeInsn(CHECKCAST, INTERNAL_NAME);
+    mv.visitVarInsn(ASTORE, 3);
+    mv.visitLabel(labels[8]);
+    mv.visitLineNumber(19, labels[8]);
+    mv.visitVarInsn(ILOAD, 1);
+    mv.visitVarInsn(ALOAD, 3);
+    mv.visitInsn(POP);
+    mv.visitMethodInsn(INVOKESTATIC, INTERNAL_NAME, "foo", "()I", false);
+    mv.visitInsn(IADD);
+    mv.visitVarInsn(ISTORE, 1);
+    mv.visitLabel(labels[9]);
+    mv.visitLineNumber(20, labels[9]);
+    mv.visitJumpInsn(GOTO, labels[7]);
+    mv.visitLabel(labels[5]);
+    mv.visitLineNumber(23, labels[5]);
+    mv.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
+    mv.visitVarInsn(ILOAD, 1);
+    mv.visitInsn(IRETURN);
+    mv.visitLabel(labels[10]);
+    mv.visitLocalVariable("b", "L" + INTERNAL_NAME + ";", null, labels[8], labels[9], 3);
+    mv.visitLocalVariable("this", "L" + INTERNAL_NAME + ";", null, labels[0], labels[10], 0);
+    mv.visitLocalVariable("a", "I", null, labels[1], labels[10], 1);
+    mv.visitMaxs(2, 4);
+    mv.visitEnd();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
new file mode 100644
index 0000000..108b474
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/KotlinDebugInfoTestRunner.java
@@ -0,0 +1,89 @@
+// 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.debuginfo;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Test;
+
+public class KotlinDebugInfoTestRunner extends TestBase {
+  private Path buildInput(byte[] clazz, String descriptor) {
+    Path inputJar = temp.getRoot().toPath().resolve("input.jar");
+    ArchiveConsumer inputJarConsumer = new ArchiveConsumer(inputJar);
+    inputJarConsumer.accept(clazz, descriptor, null);
+    inputJarConsumer.finished(null);
+    return inputJar;
+  }
+
+  private Path buildCf(Path inputJar) throws Exception {
+    Path cfJar = temp.getRoot().toPath().resolve("r8cf.jar");
+    build(inputJar, new ArchiveConsumer(cfJar));
+    return cfJar;
+  }
+
+  @Test
+  public void testRingBuffer() throws Exception {
+    // This test hits the case where we simplify a DebugLocalWrite v'(x) <- v
+    // with debug use [live: y], and y is written between v and v'.
+    // In this case we must not move [live: y] to the definition of v,
+    // since it causes the live range of y to extend to the entry to the first block.
+    test(KotlinRingBufferDump.dump(), KotlinRingBufferDump.CLASS_NAME);
+  }
+
+  @Test
+  public void testReflection() throws Exception {
+    // This test hits the case where we replace a phi(v, v) that has local info
+    // with v that has no local info.
+    test(KotlinReflectionDump.dump(), KotlinReflectionDump.CLASS_NAME);
+  }
+
+  @Test
+  public void testFoo() throws Exception {
+    test(DebugInfoDump.dump(), DebugInfoDump.CLASS_NAME);
+  }
+
+  public void test(byte[] bytes, String className) throws Exception {
+    String descriptor = 'L' + className.replace('.', '/') + ';';
+    Path inputJar = buildInput(bytes, descriptor);
+    ProcessResult runInput = ToolHelper.runJava(inputJar, className);
+    if (0 != runInput.exitCode) {
+      System.out.println(runInput);
+    }
+    assertEquals(0, runInput.exitCode);
+    Path outCf = buildCf(inputJar);
+    ProcessResult runCf = ToolHelper.runJava(outCf, className);
+    assertEquals(runInput.toString(), runCf.toString());
+  }
+
+  private void build(Path inputJar, ProgramConsumer consumer) throws Exception {
+    Builder builder =
+        R8Command.builder()
+            .setMode(CompilationMode.DEBUG)
+            .setProgramConsumer(consumer)
+            .addProgramFiles(inputJar);
+    if ((consumer instanceof ClassFileConsumer)) {
+      builder.addLibraryFiles(Paths.get(ToolHelper.JAVA_8_RUNTIME));
+    } else {
+      builder.addLibraryFiles(ToolHelper.getAndroidJar(ToolHelper.getMinApiLevelForDexVm()));
+    }
+    // TODO(b/75997473): Enable inlining when supported by CF backend
+    ToolHelper.runR8(
+        builder.build(),
+        options -> {
+          options.enableInlining = false;
+          options.invalidDebugInfoFatal = true;
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/KotlinReflectionDump.java b/src/test/java/com/android/tools/r8/debuginfo/KotlinReflectionDump.java
new file mode 100644
index 0000000..f8bddbb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/KotlinReflectionDump.java
@@ -0,0 +1,170 @@
+// 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.debuginfo;
+
+import org.objectweb.asm.*;
+
+public class KotlinReflectionDump implements Opcodes {
+
+  private static final String reflectionFactory =
+      "java/lang/Object"; // "kotlin/jvm/internal/ReflectionFactory";
+  private static final String kClass = "java/lang/Object"; // "kotlin/reflect/KClass";
+  private static final String INTERNAL_NAME = "kotlin/jvm/internal/Reflection";
+  public static final String CLASS_NAME = INTERNAL_NAME.replace('/', '.');
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    MethodVisitor mv;
+    AnnotationVisitor av0;
+
+    cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, INTERNAL_NAME, null, "java/lang/Object", null);
+
+    cw.visitSource("Reflection.java", null);
+
+    {
+      fv =
+          cw.visitField(
+              ACC_PRIVATE + ACC_FINAL + ACC_STATIC,
+              "factory",
+              "L" + reflectionFactory + ";",
+              null,
+              null);
+      fv.visitEnd();
+    }
+    {
+      fv =
+          cw.visitField(
+              ACC_FINAL + ACC_STATIC,
+              "REFLECTION_NOT_AVAILABLE",
+              "Ljava/lang/String;",
+              null,
+              " (Kotlin reflection is not available)");
+      fv.visitEnd();
+    }
+    {
+      fv =
+          cw.visitField(
+              ACC_PRIVATE + ACC_FINAL + ACC_STATIC,
+              "EMPTY_K_CLASS_ARRAY",
+              "[L" + kClass + ";",
+              null,
+              null);
+      fv.visitEnd();
+    }
+    method0(cw);
+    mainMethod(cw);
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private static void mainMethod(ClassWriter cw) {
+    MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+    mv.visitCode();
+    mv.visitInsn(RETURN);
+    mv.visitMaxs(0, 1);
+    mv.visitEnd();
+  }
+
+  private static void method0(ClassWriter cw) {
+    MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
+    Label[] labels = new Label[18];
+    for (int i = 0; i < labels.length; i++) {
+      labels[i] = new Label();
+    }
+    mv.visitCode();
+    mv.visitTryCatchBlock(labels[0], labels[1], labels[2], "java/lang/ClassCastException");
+    mv.visitTryCatchBlock(labels[0], labels[1], labels[3], "java/lang/ClassNotFoundException");
+    mv.visitTryCatchBlock(labels[0], labels[1], labels[4], "java/lang/InstantiationException");
+    mv.visitTryCatchBlock(labels[0], labels[1], labels[5], "java/lang/IllegalAccessException");
+    mv.visitLabel(labels[0]);
+    mv.visitLineNumber(33, labels[0]);
+    mv.visitLdcInsn("kotlin.reflect.jvm.internal.ReflectionFactoryImpl");
+    mv.visitMethodInsn(
+        INVOKESTATIC, "java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;", false);
+    mv.visitVarInsn(ASTORE, 1);
+    mv.visitLabel(labels[6]);
+    mv.visitLineNumber(34, labels[6]);
+    mv.visitVarInsn(ALOAD, 1);
+    mv.visitMethodInsn(
+        INVOKEVIRTUAL, "java/lang/Class", "newInstance", "()Ljava/lang/Object;", false);
+    mv.visitTypeInsn(CHECKCAST, reflectionFactory);
+    mv.visitVarInsn(ASTORE, 0);
+    mv.visitLabel(labels[1]);
+    mv.visitLineNumber(39, labels[1]);
+    mv.visitJumpInsn(GOTO, labels[7]);
+    mv.visitLabel(labels[2]);
+    mv.visitLineNumber(36, labels[2]);
+    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/ClassCastException"});
+    mv.visitVarInsn(ASTORE, 1);
+    mv.visitLabel(labels[8]);
+    mv.visitInsn(ACONST_NULL);
+    mv.visitVarInsn(ASTORE, 0);
+    mv.visitLabel(labels[9]);
+    mv.visitLineNumber(39, labels[9]);
+    mv.visitJumpInsn(GOTO, labels[7]);
+    mv.visitLabel(labels[3]);
+    mv.visitLineNumber(37, labels[3]);
+    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/ClassNotFoundException"});
+    mv.visitVarInsn(ASTORE, 1);
+    mv.visitLabel(labels[10]);
+    mv.visitInsn(ACONST_NULL);
+    mv.visitVarInsn(ASTORE, 0);
+    mv.visitLabel(labels[11]);
+    mv.visitLineNumber(39, labels[11]);
+    mv.visitJumpInsn(GOTO, labels[7]);
+    mv.visitLabel(labels[4]);
+    mv.visitLineNumber(38, labels[4]);
+    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/InstantiationException"});
+    mv.visitVarInsn(ASTORE, 1);
+    mv.visitLabel(labels[12]);
+    mv.visitInsn(ACONST_NULL);
+    mv.visitVarInsn(ASTORE, 0);
+    mv.visitLabel(labels[13]);
+    mv.visitLineNumber(39, labels[13]);
+    mv.visitJumpInsn(GOTO, labels[7]);
+    mv.visitLabel(labels[5]);
+    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/IllegalAccessException"});
+    mv.visitVarInsn(ASTORE, 1);
+    mv.visitLabel(labels[14]);
+    mv.visitInsn(ACONST_NULL);
+    mv.visitVarInsn(ASTORE, 0);
+    mv.visitLabel(labels[7]);
+    mv.visitLineNumber(41, labels[7]);
+    mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] {reflectionFactory}, 0, null);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitJumpInsn(IFNULL, labels[15]);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitJumpInsn(GOTO, labels[16]);
+    mv.visitLabel(labels[15]);
+    mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+    mv.visitTypeInsn(NEW, reflectionFactory);
+    mv.visitInsn(DUP);
+    mv.visitMethodInsn(INVOKESPECIAL, reflectionFactory, "<init>", "()V", false);
+    mv.visitLabel(labels[16]);
+    mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {reflectionFactory});
+    mv.visitFieldInsn(PUTSTATIC, INTERNAL_NAME, "factory", "L" + reflectionFactory + ";");
+    mv.visitLabel(labels[17]);
+    mv.visitLineNumber(46, labels[17]);
+    mv.visitInsn(ICONST_0);
+    mv.visitTypeInsn(ANEWARRAY, kClass);
+    mv.visitFieldInsn(PUTSTATIC, INTERNAL_NAME, "EMPTY_K_CLASS_ARRAY", "[L" + kClass + ";");
+    mv.visitInsn(RETURN);
+    mv.visitLocalVariable(
+        "implClass", "Ljava/lang/Class;", "Ljava/lang/Class<*>;", labels[6], labels[1], 1);
+    mv.visitLocalVariable("e", "Ljava/lang/ClassCastException;", null, labels[8], labels[9], 1);
+    mv.visitLocalVariable(
+        "e", "Ljava/lang/ClassNotFoundException;", null, labels[10], labels[11], 1);
+    mv.visitLocalVariable(
+        "e", "Ljava/lang/InstantiationException;", null, labels[12], labels[13], 1);
+    mv.visitLocalVariable(
+        "e", "Ljava/lang/IllegalAccessException;", null, labels[14], labels[7], 1);
+    mv.visitLocalVariable("impl", "L" + reflectionFactory + ";", null, labels[1], labels[17], 0);
+    mv.visitMaxs(2, 2);
+    mv.visitEnd();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debuginfo/KotlinRingBufferDump.java b/src/test/java/com/android/tools/r8/debuginfo/KotlinRingBufferDump.java
new file mode 100644
index 0000000..11b981a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/KotlinRingBufferDump.java
@@ -0,0 +1,195 @@
+// 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.debuginfo;
+
+import org.objectweb.asm.*;
+
+public class KotlinRingBufferDump implements Opcodes {
+
+  public static final String INTERNAL_NAME = "kotlin/collections/RingBuffer";
+  public static final String DESCRIPTOR = "L" + INTERNAL_NAME + ";";
+  public static final String CLASS_NAME = "kotlin.collections.RingBuffer";
+
+  public static byte[] dump() throws Exception {
+
+    ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
+    AnnotationVisitor av0;
+
+    String superName = "java/lang/Object"; // "kotlin/collections/AbstractList";
+    String signature =
+        null; // "<T:Ljava/lang/Object;>Lkotlin/collections/AbstractList<TT;>;Ljava/util/RandomAccess;";
+    cw.visit(
+        V1_6,
+        ACC_FINAL + ACC_SUPER + ACC_PUBLIC,
+        INTERNAL_NAME,
+        signature,
+        superName,
+        new String[] {"java/util/RandomAccess"});
+
+    {
+      av0 = cw.visitAnnotation("Lkotlin/Metadata;", true);
+      av0.visit("mv", new int[] {1, 1, 9});
+      av0.visit("bv", new int[] {1, 0, 2});
+      av0.visit("k", new Integer(1));
+      {
+        AnnotationVisitor av1 = av0.visitArray("d1");
+        av1.visit(
+            null,
+            "\u0000>\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0002\n\u0002\u0010\u0011\n\u0002\u0010\u0000\n\u0002\u0008\u0009\n\u0002\u0010\u0002\n\u0002\u0008\u0006\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010(\n\u0002\u0008\u000c\u0008\u0002\u0018\u0000*\u0004\u0008\u0000\u0010\u00012\u0008\u0012\u0004\u0012\u0002H\u00010\u00022\u00060\u0003j\u0002`\u0004B\r\u0012\u0006\u0010\u0005\u001a\u00020\u0006\u00a2\u0006\u0002\u0010\u0007J\u0013\u0010\u0013\u001a\u00020\u00142\u0006\u0010\u0015\u001a\u00028\u0000\u00a2\u0006\u0002\u0010\u0016J\u0016\u0010\u0017\u001a\u00028\u00002\u0006\u0010\u0018\u001a\u00020\u0006H\u0096\u0002\u00a2\u0006\u0002\u0010\u0019J\u0006\u0010\u001a\u001a\u00020\u001bJ\u000f\u0010\u001c\u001a\u0008\u0012\u0004\u0012\u00028\u00000\u001dH\u0096\u0002J\u000e\u0010\u001e\u001a\u00020\u00142\u0006\u0010\u001f\u001a\u00020\u0006J\u0015\u0010 \u001a\n\u0012\u0006\u0012\u0004\u0018\u00010\n0\u0009H\u0014\u00a2\u0006\u0002\u0010!J'\u0010 \u001a\u0008\u0012\u0004\u0012\u0002H\u00010\u0009\"\u0004\u0008\u0001\u0010\u00012\u000c\u0010\"\u001a\u0008\u0012\u0004\u0012\u0002H\u00010\u0009H\u0015\u00a2\u0006\u0002\u0010#J9\u0010$\u001a\u00020\u0014\"\u0004\u0008\u0001\u0010\u0001*\u0008\u0012\u0004\u0012\u0002H\u00010\u00092\u0006\u0010\u0015\u001a\u0002H\u00012\u0008\u0008\u0002\u0010%\u001a\u00020\u00062\u0008\u0008\u0002\u0010&\u001a\u00020\u0006H\u0002\u00a2\u0006\u0002\u0010'J\u0015\u0010(\u001a\u00020\u0006*\u00020\u00062\u0006\u0010\u001f\u001a\u00020\u0006H\u0083\u0008R\u0018\u0010\u0008\u001a\n\u0012\u0006\u0012\u0004\u0018\u00010\n0\u0009X\u0082\u0004\u00a2\u0006\u0004\n\u0002\u0010\u000bR\u0011\u0010\u0005\u001a\u00020\u0006\u00a2\u0006\u0008\n\u0000\u001a\u0004\u0008\u000c\u0010\rR$\u0010\u000f\u001a\u00020\u00062\u0006\u0010\u000e\u001a\u00020\u0006@RX\u0096\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008\u0010\u0010\r\"\u0004\u0008\u0011\u0010\u0007R\u000e\u0010\u0012\u001a\u00020\u0006X\u0082\u000e\u00a2\u0006\u0002\n\u0000\u00a8\u0006)");
+        av1.visitEnd();
+      }
+      {
+        AnnotationVisitor av1 = av0.visitArray("d2");
+        av1.visit(null, DESCRIPTOR);
+        av1.visit(null, "T");
+        av1.visit(null, "Lkotlin/collections/AbstractList;");
+        av1.visit(null, "Ljava/util/RandomAccess;");
+        av1.visit(null, "Lkotlin/collections/RandomAccess;");
+        av1.visit(null, "capacity");
+        av1.visit(null, "");
+        av1.visit(null, "(I)V");
+        av1.visit(null, "buffer");
+        av1.visit(null, "");
+        av1.visit(null, "");
+        av1.visit(null, "[Ljava/lang/Object;");
+        av1.visit(null, "getCapacity");
+        av1.visit(null, "()I");
+        av1.visit(null, "<set-?>");
+        av1.visit(null, "size");
+        av1.visit(null, "getSize");
+        av1.visit(null, "setSize");
+        av1.visit(null, "startIndex");
+        av1.visit(null, "add");
+        av1.visit(null, "");
+        av1.visit(null, "element");
+        av1.visit(null, "(Ljava/lang/Object;)V");
+        av1.visit(null, "get");
+        av1.visit(null, "index");
+        av1.visit(null, "(I)Ljava/lang/Object;");
+        av1.visit(null, "isFull");
+        av1.visit(null, "");
+        av1.visit(null, "iterator");
+        av1.visit(null, "");
+        av1.visit(null, "removeFirst");
+        av1.visit(null, "n");
+        av1.visit(null, "toArray");
+        av1.visit(null, "()[Ljava/lang/Object;");
+        av1.visit(null, "array");
+        av1.visit(null, "([Ljava/lang/Object;)[Ljava/lang/Object;");
+        av1.visit(null, "fill");
+        av1.visit(null, "fromIndex");
+        av1.visit(null, "toIndex");
+        av1.visit(null, "([Ljava/lang/Object;Ljava/lang/Object;II)V");
+        av1.visit(null, "forward");
+        av1.visit(null, "kotlin-stdlib");
+        av1.visitEnd();
+      }
+      av0.visitEnd();
+    }
+    cw.visitInnerClass(
+        INTERNAL_NAME + "$iterator$1", null, null, ACC_PUBLIC + ACC_FINAL + ACC_STATIC);
+
+    {
+      fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "buffer", "[Ljava/lang/Object;", null, null);
+      fv.visitEnd();
+    }
+    {
+      fv = cw.visitField(ACC_PRIVATE, "startIndex", "I", null, null);
+      fv.visitEnd();
+    }
+    {
+      fv = cw.visitField(ACC_PRIVATE, "size", "I", null, null);
+      fv.visitEnd();
+    }
+    {
+      fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "capacity", "I", null, null);
+      fv.visitEnd();
+    }
+    methodAdd(cw);
+    methodMain(cw);
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+
+  private static void methodMain(ClassWriter cw) {
+    MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+    mv.visitCode();
+    mv.visitInsn(RETURN);
+    mv.visitMaxs(0, 1);
+    mv.visitEnd();
+  }
+
+  private static void methodAdd(ClassWriter cw) {
+    MethodVisitor mv =
+        cw.visitMethod(ACC_PUBLIC + ACC_FINAL, "add", "(Ljava/lang/Object;)V", "(TT;)V", null);
+    Label[] labels = new Label[8];
+    for (int i = 0; i < labels.length; i++) {
+      labels[i] = new Label();
+    }
+    mv.visitCode();
+    mv.visitLabel(labels[0]);
+    mv.visitLineNumber(169, labels[0]);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_NAME, "isFull", "()Z", false);
+    mv.visitJumpInsn(IFEQ, labels[1]);
+    mv.visitLabel(labels[2]);
+    mv.visitLineNumber(170, labels[2]);
+    mv.visitTypeInsn(NEW, "java/lang/IllegalStateException");
+    mv.visitInsn(DUP);
+    mv.visitLdcInsn("ring buffer is full");
+    mv.visitMethodInsn(
+        INVOKESPECIAL, "java/lang/IllegalStateException", "<init>", "(Ljava/lang/String;)V", false);
+    mv.visitTypeInsn(CHECKCAST, "java/lang/Throwable");
+    mv.visitInsn(ATHROW);
+    mv.visitLabel(labels[1]);
+    mv.visitLineNumber(173, labels[1]);
+    mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "buffer", "[Ljava/lang/Object;");
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitFieldInsn(GETFIELD, INTERNAL_NAME, "startIndex", "I");
+    mv.visitVarInsn(ISTORE, 3);
+    mv.visitVarInsn(ASTORE, 2);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_NAME, "size", "()I", false);
+    mv.visitVarInsn(ISTORE, 4);
+    mv.visitLabel(labels[3]);
+    mv.visitLineNumber(212, labels[3]);
+    mv.visitVarInsn(ILOAD, 3);
+    mv.visitVarInsn(ILOAD, 4);
+    mv.visitInsn(IADD);
+    mv.visitVarInsn(ALOAD, 2);
+    mv.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_NAME, "getCapacity", "()I", false);
+    mv.visitInsn(IREM);
+    mv.visitLabel(labels[4]);
+    mv.visitVarInsn(ALOAD, 1);
+    mv.visitInsn(AASTORE);
+    mv.visitLabel(labels[5]);
+    mv.visitLineNumber(174, labels[5]);
+    mv.visitVarInsn(ALOAD, 0);
+    mv.visitInsn(DUP);
+    mv.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_NAME, "size", "()I", false);
+    mv.visitInsn(DUP);
+    mv.visitVarInsn(ISTORE, 2);
+    mv.visitInsn(ICONST_1);
+    mv.visitInsn(IADD);
+    mv.visitMethodInsn(INVOKESPECIAL, INTERNAL_NAME, "setSize", "(I)V", false);
+    mv.visitLabel(labels[6]);
+    mv.visitLineNumber(175, labels[6]);
+    mv.visitInsn(RETURN);
+    mv.visitLabel(labels[7]);
+    mv.visitLocalVariable("this_$iv", DESCRIPTOR, null, labels[3], labels[4], 2);
+    mv.visitLocalVariable("$receiver$iv", "I", null, labels[3], labels[4], 3);
+    mv.visitLocalVariable("n$iv", "I", null, labels[3], labels[4], 4);
+    mv.visitLocalVariable("$i$f$forward", "I", null, labels[3], labels[4], 5);
+    mv.visitLocalVariable("this", DESCRIPTOR, null, labels[0], labels[7], 0);
+    mv.visitLocalVariable("element", "Ljava/lang/Object;", null, labels[0], labels[7], 1);
+    mv.visitMaxs(3, 6);
+    mv.visitEnd();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
index 55195a4..8102882 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/IdenticalAfterRegisterAllocationTest.java
@@ -34,11 +34,6 @@
     }
 
     @Override
-    public boolean argumentValueUsesHighRegister(Value value, int instructionNumber) {
-      return false;
-    }
-
-    @Override
     public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
       return value.getNumber();
     }
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index ff3f81a..e5838c3 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -111,6 +111,22 @@
       return "L" + name + ";";
     }
 
+    public MethodSignature addAbstractMethod(
+        String name,
+        List<String> argumentTypes,
+        String returnType) {
+      return addMethod("public abstract", name, argumentTypes, returnType);
+    }
+
+    public MethodSignature addFinalMethod(
+        String name,
+        List<String> argumentTypes,
+        String returnType,
+        String... lines) {
+      makeInit = true;
+      return addMethod("public final", name, argumentTypes, returnType, lines);
+    }
+
     public MethodSignature addVirtualMethod(
         String name,
         List<String> argumentTypes,
@@ -238,7 +254,7 @@
       isInterface = true;
     }
 
-    void setAccess(String access) {
+    public void setAccess(String access) {
       this.access = access;
     }
 
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index a6d0177..50a3c06 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -676,7 +676,12 @@
     }
 
     @Override
-    public DebugLocalInfo getCurrentLocal(int register) {
+    public DebugLocalInfo getIncomingLocal(int register) {
+      return null;
+    }
+
+    @Override
+    public DebugLocalInfo getOutgoingLocal(int register) {
       return null;
     }
 
@@ -686,12 +691,6 @@
     }
 
     @Override
-    public void closingCurrentBlockWithFallthrough(
-        int fallthroughInstructionIndex, IRBuilder builder) {
-      throw new Unreachable();
-    }
-
-    @Override
     public void setUp() {
       // Intentionally empty.
     }
diff --git a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
index fa39257..f27c88a 100644
--- a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
@@ -303,9 +303,10 @@
   private static int countRenamedClassIdentifier(
       DexInspector inspector, DexEncodedField[] fields) {
     return Arrays.stream(fields)
-        .filter(encodedField -> encodedField.staticValue instanceof DexValueString)
+        .filter(encodedField -> encodedField.getStaticValue() instanceof DexValueString)
         .reduce(0, (cnt, encodedField) -> {
-          String cnstString = ((DexValueString) encodedField.staticValue).getValue().toString();
+          String cnstString =
+              ((DexValueString) encodedField.getStaticValue()).getValue().toString();
           if (isValidJavaType(cnstString)) {
             ClassSubject classSubject = inspector.clazz(cnstString);
             if (classSubject.isRenamed()
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
index eb9bf2e..2e4d6cf 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
@@ -4,8 +4,13 @@
 package com.android.tools.r8.regress.b78493232;
 
 import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import org.junit.Assume;
 import org.junit.Test;
 
@@ -21,4 +26,18 @@
         Regress78493232Dump.dump(),
         ToolHelper.getClassAsBytes(Regress78493232Utils.class));
   }
+
+  // Main method to build a test jar for testing on device.
+  public static void main(String[] args) throws CompilationFailedException, IOException {
+    Path output = args.length > 0
+        ? Paths.get(args[0])
+        : Paths.get("Regress78493232.jar");
+    ArchiveConsumer consumer = new ArchiveConsumer(output);
+    consumer.accept(Regress78493232Dump.dump(), Regress78493232Dump.CLASS_DESC, null);
+    consumer.accept(
+        ToolHelper.getClassAsBytes(Regress78493232Utils.class),
+        Regress78493232Dump.UTILS_CLASS_DESC,
+        null);
+    consumer.finished(null);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump.java
index ba7ec6b..8bff3c7 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump.java
@@ -117,49 +117,21 @@
       mv.visitLdcInsn(new Integer(iterations));
       Label l1 = new Label();
       mv.visitJumpInsn(IF_ICMPGE, l1);
-      {
-        mv.visitMethodInsn(INVOKESTATIC, CLASS_INTERNAL, "run", "()Ljava/lang/String;", false);
-
-        mv.visitVarInsn(ASTORE, 2);
-        mv.visitVarInsn(ALOAD, 2);
-        mv.visitLdcInsn("java.security.SecureRandom");
-        mv.visitMethodInsn(
-            INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
-        Label l2 = new Label();
-        mv.visitJumpInsn(IFNE, l2);
-        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
-        mv.visitLdcInsn("result incorrect: ");
-        mv.visitMethodInsn(
-            INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
-        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
-        mv.visitVarInsn(ALOAD, 2);
-        mv.visitMethodInsn(
-            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
-        mv.visitInsn(ICONST_1);
-        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "exit", "(I)V", false);
-        mv.visitInsn(RETURN);
-
-        mv.visitLabel(l2);
-        getHash(mv);
-        mv.visitLdcInsn(new Integer(419176645));
-        Label l3 = new Label();
-        mv.visitJumpInsn(IF_ICMPEQ, l3);
-        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
-        mv.visitLdcInsn("state incorrect: ");
-        mv.visitMethodInsn(
-            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
-        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
-        printState(mv);
-        mv.visitInsn(ICONST_2);
-        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "exit", "(I)V", false);
-        mv.visitInsn(RETURN);
-        mv.visitLabel(l3);
-      }
+      mv.visitMethodInsn(INVOKESTATIC, CLASS_INTERNAL, "run", "()Ljava/lang/String;", false);
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitMethodInsn(
+          INVOKESTATIC, UTILS_CLASS_INTERNAL, "compare", "(Ljava/lang/String;I)V", false);
+      mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+      mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+      mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "compareHash", "(II[BI)V", false);
       mv.visitIincInsn(1, 1);
       mv.visitJumpInsn(GOTO, l0);
       mv.visitLabel(l1);
-
-      println(mv, "Completed successfully after " + iterations + " iterations");
+      mv.visitLdcInsn("Completed successfully after " + iterations + " iterations");
+      mv.visitMethodInsn(
+          INVOKESTATIC, UTILS_CLASS_INTERNAL, "println", "(Ljava/lang/String;)V", false);
       mv.visitInsn(RETURN);
       mv.visitMaxs(-1, -1);
       mv.visitEnd();
@@ -335,20 +307,6 @@
     return cw.toByteArray();
   }
 
-  private static void printState(MethodVisitor mv) {
-    println(mv, "staticIntA:");
-    mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
-    printlnInt(mv);
-
-    println(mv, "staticIntB:");
-    mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
-    printlnInt(mv);
-
-    println(mv, "staticByteArray:");
-    mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
-    printByteArray(mv);
-  }
-
   private static void getHash(MethodVisitor mv) {
     mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
     mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
@@ -356,21 +314,4 @@
     mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "getHash", "(II[B)I", false);
   }
 
-  private static void printByteArray(MethodVisitor mv) {
-    mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "printByteArray", "([B)V", false);
-  }
-
-  private static void printlnInt(MethodVisitor mv) {
-    mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "println", "(I)V", false);
-  }
-
-  private static void printlnString(MethodVisitor mv) {
-    mv.visitMethodInsn(
-        INVOKESTATIC, UTILS_CLASS_INTERNAL, "println", "(Ljava/lang/String;)V", false);
-  }
-
-  private static void println(MethodVisitor mv, String msg) {
-    mv.visitLdcInsn(msg);
-    printlnString(mv);
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
index 14b17ce..40d979e 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
@@ -9,15 +9,7 @@
 
 public class Regress78493232Utils {
 
-  public static void println(String msg) {
-    System.out.println(msg);
-  }
-
-  public static void println(int msg) {
-    System.out.println(msg);
-  }
-
-  public static void printByteArray(byte[] array) {
+  private static void printByteArray(byte[] array) {
     List<String> strings = new ArrayList<>(array.length);
     for (byte b : array) {
       strings.add(Byte.toString(b));
@@ -29,7 +21,30 @@
     return a + 7 * b + 13 * Arrays.hashCode(c);
   }
 
-  public static void printHash(int a, int b, byte[] c) {
-    System.out.println(getHash(a, b, c));
+  public static void compare(String output, int iterations) {
+    String expected = "java.security.SecureRandom";
+    if (output.equals(expected)) {
+      return;
+    }
+    System.out.println(
+        "After " + iterations + " iterations, expected \"" +
+        expected + "\", but got \"" + output + "\"");
+    System.exit(1);
+  }
+
+  public static void compareHash(int a, int b, byte[] c, int iterations) {
+    int expected = 419176645;
+    int output = getHash(a, b, c);
+    if (output == expected) {
+      return;
+    }
+    System.out.println(
+        "After " + iterations + " iterations, expected hash " +
+        expected + ", but got " + output);
+    System.out.println("staticIntA: " + a);
+    System.out.println("staticIntB: " + b);
+    System.out.print("staticIntByteArray: ");
+    printByteArray(c);
+    System.exit(1);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
index 51772fe..087b5f8 100644
--- a/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/staticvalues/StaticValuesTest.java
@@ -100,47 +100,47 @@
         inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
 
     DexValue value;
-    assertTrue(inspector.clazz("Test").field("boolean", "booleanField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("boolean", "booleanField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("boolean", "booleanField").getStaticValue();
     assertTrue(value instanceof DexValueBoolean);
     assertEquals(true, ((DexValueBoolean) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("byte", "byteField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("byte", "byteField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("byte", "byteField").getStaticValue();
     assertTrue(value instanceof DexValueByte);
     assertEquals(1, ((DexValueByte) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("short", "shortField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("short", "shortField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("short", "shortField").getStaticValue();
     assertTrue(value instanceof DexValueShort);
     assertEquals(2, ((DexValueShort) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("int", "intField").getStaticValue();
     assertTrue(value instanceof DexValueInt);
     assertEquals(3, ((DexValueInt) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("long", "longField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("long", "longField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("long", "longField").getStaticValue();
     assertTrue(value instanceof DexValueLong);
     assertEquals(4, ((DexValueLong) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("float", "floatField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("float", "floatField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("float", "floatField").getStaticValue();
     assertTrue(value instanceof DexValueFloat);
     assertEquals(5.0f, ((DexValueFloat) value).getValue(), 0.0);
 
-    assertTrue(inspector.clazz("Test").field("double", "doubleField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("double", "doubleField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("double", "doubleField").getStaticValue();
     assertTrue(value instanceof DexValueDouble);
     assertEquals(6.0f, ((DexValueDouble) value).getValue(), 0.0);
 
-    assertTrue(inspector.clazz("Test").field("char", "charField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("char", "charField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("char", "charField").getStaticValue();
     assertTrue(value instanceof DexValueChar);
     assertEquals(0x30 + 7, ((DexValueChar) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
     assertTrue(value instanceof DexValueString);
     assertEquals(("8"), ((DexValueString) value).getValue().toString());
@@ -317,12 +317,12 @@
         inspector.clazz("Test").clinit().getMethod().getCode().asDexCode().isEmptyVoidMethod());
 
     DexValue value;
-    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("int", "intField").getStaticValue();
     assertTrue(value instanceof DexValueInt);
     assertEquals(3, ((DexValueInt) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
     assertTrue(value instanceof DexValueString);
     assertEquals(("7"), ((DexValueString) value).getValue().toString());
@@ -388,12 +388,12 @@
     assertTrue(inspector.clazz("Test").clinit().isPresent());
 
     DexValue value;
-    assertTrue(inspector.clazz("Test").field("int", "intField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("int", "intField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("int", "intField").getStaticValue();
     assertTrue(value instanceof DexValueInt);
     assertEquals(3, ((DexValueInt) value).getValue());
 
-    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasStaticValue());
+    assertTrue(inspector.clazz("Test").field("java.lang.String", "stringField").hasExplicitStaticValue());
     value = inspector.clazz("Test").field("java.lang.String", "stringField").getStaticValue();
     assertTrue(value instanceof DexValueString);
     assertEquals(("7"), ((DexValueString) value).getValue().toString());
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index 0cea762..29676da 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -821,7 +821,7 @@
   }
 
   public abstract class FieldSubject extends MemberSubject {
-    public abstract boolean hasStaticValue();
+    public abstract boolean hasExplicitStaticValue();
 
     public abstract DexEncodedField getField();
 
@@ -863,7 +863,7 @@
     }
 
     @Override
-    public boolean hasStaticValue() {
+    public boolean hasExplicitStaticValue() {
       return false;
     }
 
@@ -928,13 +928,13 @@
     }
 
     @Override
-    public boolean hasStaticValue() {
-      return dexField.staticValue != null;
+    public boolean hasExplicitStaticValue() {
+      return isStatic() && dexField.hasExplicitStaticValue();
     }
 
     @Override
     public DexValue getStaticValue() {
-      return dexField.staticValue;
+      return dexField.getStaticValue();
     }
 
     @Override
diff --git a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
index eff632a..f41be5c 100644
--- a/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
+++ b/src/test/java/com/android/tools/r8/utils/FeatureClassMappingTest.java
@@ -94,6 +94,7 @@
     assertEquals(mapping.featureForClass("com.google.Feature1"), "feature1");
     assertEquals(mapping.featureForClass("com.google.different.Feature1"), "base");
     assertEquals(mapping.featureForClass("com.strange.different.Feature1"), "feature2");
+    assertEquals(mapping.featureForClass("com.stranger.Clazz"), "base");
     assertEquals(mapping.featureForClass("Feature1"), "base");
     assertEquals(mapping.featureForClass("a.b.z.A"), "base");
   }
@@ -115,4 +116,17 @@
     ensureThrowsMappingException(
         ImmutableList.of("com.google.foo.*:feature1", "com.google.foo.*:feature2"));
   }
+
+  @Test
+  public void testUsesOnlyExactMappings() throws Exception {
+    List<String> lines =
+        ImmutableList.of(
+            "com.pkg1.Clazz:feature1",
+            "com.pkg2.Clazz:feature2");
+    FeatureClassMapping mapping = new FeatureClassMapping(lines);
+
+    assertEquals(mapping.featureForClass("com.pkg1.Clazz"), "feature1");
+    assertEquals(mapping.featureForClass("com.pkg2.Clazz"), "feature2");
+    assertEquals(mapping.featureForClass("com.pkg1.Other"), mapping.baseName);
+  }
 }