IRBuilder: Distinguish incoming and outgoing locals for instructions

Split SourceCode.getCurrentLocal() into getIncomingLocal() and
getOutgoingLocal().  Similarly, split IRBuilder.getLocalValue() into
getIncomingLocalValue() and getOutgoingLocalValue() (which defer to the
SourceCode methods).

Allow calling IRBuilder.addDebugLocalStart(register, local) in the no-op
case when the most recent instruction introduced the local to start.
This makes it easier for JarSourceCode since it can just start all
locals, even those that were just written with new local info.

Make JarState transactional around each instruction: Before building an
instruction, JarState.beginTransaction() is called which reads the local
variable table to find locals that start/end right after the instruction
to build. JarState.writeLocal() doesn't update local information until
JarState.endTransaction() is called, at which point JarState verifies
that the updated local information matches the register writes.

Other small changes:

* CodeRewriter.simplifyDebugLocals(): Don't simplify if the local being
  simplified changes in-between.

* CodeRewriter.simplifyDebugLocals(): Move unrelated debug uses to
  previous instruction instead of inValue.definition.

* DexType.isInterface(): More helpful assertion error.

* JarState.pop(): More helpful assertion error.

* CfRegisterAllocator: Assert that unhandled does not contain Argument.

* Add more checks in IRCode.consistentBlockInstructions().

* JumpInstruction.toString(): Catch AssertionError in getNumber().

* Value.replaceDebugUser(): Remove DebugUse.START when moving it to its
  definition

Bug: 77522100
Change-Id: I808ca8c07135538f5764a70d197f4b54301d7743
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 923ac4a..b4a6009 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -138,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/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/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/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/Switch.java b/src/main/java/com/android/tools/r8/ir/code/Switch.java
index e8baacc..b8f90e6 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
@@ -265,7 +265,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 -> ");
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/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/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/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 16050b2..7073215 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;
@@ -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/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/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/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/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/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/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.
     }