Merge "Minor refactor of -identifiernamestring warnings"
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 b4a6009..22bbf46 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRCode.LiveAtEntrySets;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.Phi;
@@ -25,7 +26,6 @@
 import java.util.Map;
 import java.util.NavigableSet;
 import java.util.PriorityQueue;
-import java.util.Set;
 import java.util.TreeSet;
 
 /**
@@ -41,7 +41,7 @@
   private final InternalOptions options;
 
   // Mapping from basic blocks to the set of values live at entry to that basic block.
-  private Map<BasicBlock, Set<Value>> liveAtEntrySets;
+  private Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets;
 
   // List of all top-level live intervals for all SSA values.
   private final List<LiveIntervals> liveIntervals = new ArrayList<>();
@@ -106,7 +106,7 @@
     ImmutableList<BasicBlock> blocks = computeLivenessInformation();
     performLinearScan();
     if (options.debug) {
-      LinearScanRegisterAllocator.computeDebugInfo(blocks, liveIntervals, this);
+      LinearScanRegisterAllocator.computeDebugInfo(blocks, liveIntervals, this, liveAtEntrySets);
     }
   }
 
@@ -252,10 +252,10 @@
   }
 
   public void addToLiveAtEntrySet(BasicBlock block, Collection<Phi> phis) {
-    liveAtEntrySets.get(block).addAll(phis);
+    liveAtEntrySets.get(block).liveValues.addAll(phis);
   }
 
   public Collection<Value> getLocalsAtBlockEntry(BasicBlock block) {
-    return liveAtEntrySets.get(block);
+    return liveAtEntrySets.get(block).liveValues;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
index 644f2c2..71900a9 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
@@ -41,7 +41,8 @@
 
   @Override
   public void buildIR(IRBuilder builder, CfState state, CfSourceCode code) {
-    state.setPosition(position);
-    builder.addDebugPosition(position);
+    Position canonical = code.getCanonicalPosition(position);
+    state.setPosition(canonical);
+    builder.addDebugPosition(canonical);
   }
 }
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 d588ab5..bd67a45 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -65,6 +65,11 @@
     public CfLabel getEnd() {
       return end;
     }
+
+    @Override
+    public String toString() {
+      return "" + index + " => " + local;
+    }
   }
 
   private final DexMethod method;
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
index 6e4c464..ff126c6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalWrite.java
@@ -14,8 +14,10 @@
  *
  * <p>All instructions may have attached local information (defined as the local information of
  * their outgoing value). This instruction is needed to mark a transition of an existing value (with
- * a possible local attached) to a new value that has a local (possibly the same one). If all
- * ingoing values end up having the same local this can be safely removed.
+ * a possible local attached) to a new value that has a local (possibly the same one). Even if the
+ * debug info of the ingoing value is equal to that of the outgoing value, the write may still be
+ * needed since an explicit end may have ended the visiblity range of the local which now becomes
+ * visible again.
  *
  * <p>For valid debug info, this instruction should have at least one debug user, denoting the end
  * of its range, and thus it should be live.
@@ -25,7 +27,6 @@
   public DebugLocalWrite(Value dest, Value src) {
     super(dest, src);
     assert dest.hasLocalInfo();
-    assert dest.getLocalInfo() != src.getLocalInfo() || src.isPhi();
   }
 
   @Override
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 72728a8..0e2bc36 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
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.utils.CfgPrinter;
@@ -25,6 +26,35 @@
 
 public class IRCode {
 
+  public static class LiveAtEntrySets {
+    // Set of live SSA values (regardless of whether they denote a local variable).
+    public final Set<Value> liveValues;
+
+    // Subset of live local-variable values.
+    public final Set<Value> liveLocalValues;
+
+    public LiveAtEntrySets(Set<Value> liveValues, Set<Value> liveLocalValues) {
+      assert liveValues.containsAll(liveLocalValues);
+      this.liveValues = liveValues;
+      this.liveLocalValues = liveLocalValues;
+    }
+
+    @Override
+    public int hashCode() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      LiveAtEntrySets other = (LiveAtEntrySets) o;
+      return liveValues.equals(other.liveValues) && liveLocalValues.equals(other.liveLocalValues);
+    }
+
+    public boolean isEmpty() {
+      return liveValues.isEmpty() && liveLocalValues.isEmpty();
+    }
+  }
+
   // Stack marker to denote when all successors of a block have been processed when topologically
   // sorting.
   private static class BlockMarker {
@@ -72,8 +102,8 @@
   /**
    * Compute the set of live values at the entry to each block using a backwards data-flow analysis.
    */
-  public Map<BasicBlock, Set<Value>> computeLiveAtEntrySets() {
-    Map<BasicBlock, Set<Value>> liveAtEntrySets = new IdentityHashMap<>();
+  public Map<BasicBlock, LiveAtEntrySets> computeLiveAtEntrySets() {
+    Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets = new IdentityHashMap<>();
     Queue<BasicBlock> worklist = new ArrayDeque<>();
     // Since this is a backwards data-flow analysis we process the blocks in reverse
     // topological order to reduce the number of iterations.
@@ -82,24 +112,37 @@
     while (!worklist.isEmpty()) {
       BasicBlock block = worklist.poll();
       Set<Value> live = new HashSet<>();
+      Set<Value> liveLocals = new HashSet<>();
       for (BasicBlock succ : block.getSuccessors()) {
-        Set<Value> succLiveAtEntry = liveAtEntrySets.get(succ);
-        if (succLiveAtEntry != null) {
-          live.addAll(succLiveAtEntry);
+        LiveAtEntrySets liveAtSucc = liveAtEntrySets.get(succ);
+        if (liveAtSucc != null) {
+          live.addAll(liveAtSucc.liveValues);
+          liveLocals.addAll(liveAtSucc.liveLocalValues);
         }
         int predIndex = succ.getPredecessors().indexOf(block);
         for (Phi phi : succ.getPhis()) {
-          live.add(phi.getOperand(predIndex));
+          Value operand = phi.getOperand(predIndex);
+          live.add(operand);
+          // TODO(zerny): Assert that operand.hasLocalInfo iff phi.hasLocalInfo
+          if (operand.hasLocalInfo()) {
+            liveLocals.add(operand);
+          }
           assert phi.getDebugValues().stream().allMatch(Value::needsRegister);
+          assert phi.getDebugValues().stream().allMatch(Value::hasLocalInfo);
           live.addAll(phi.getDebugValues());
+          liveLocals.addAll(phi.getDebugValues());
         }
       }
-      ListIterator<Instruction> iterator =
-          block.getInstructions().listIterator(block.getInstructions().size());
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        if (instruction.outValue() != null) {
-          live.remove(instruction.outValue());
+      Iterator<Instruction> iterator = block.getInstructions().descendingIterator();
+      while (iterator.hasNext()) {
+        Instruction instruction = iterator.next();
+        Value outValue = instruction.outValue();
+        if (outValue != null) {
+          live.remove(outValue);
+          assert outValue.hasLocalInfo() || !liveLocals.contains(outValue);
+          if (outValue.hasLocalInfo()) {
+            liveLocals.remove(outValue);
+          }
         }
         for (Value use : instruction.inValues()) {
           if (use.needsRegister()) {
@@ -107,15 +150,22 @@
           }
         }
         assert instruction.getDebugValues().stream().allMatch(Value::needsRegister);
+        assert instruction.getDebugValues().stream().allMatch(Value::hasLocalInfo);
         live.addAll(instruction.getDebugValues());
+        liveLocals.addAll(instruction.getDebugValues());
       }
       for (Phi phi : block.getPhis()) {
         live.remove(phi);
+        assert phi.hasLocalInfo() || !liveLocals.contains(phi);
+        if (phi.hasLocalInfo()) {
+          liveLocals.remove(phi);
+        }
       }
-      Set<Value> previousLiveAtEntry = liveAtEntrySets.put(block, live);
+      LiveAtEntrySets liveAtEntry = new LiveAtEntrySets(live, liveLocals);
+      LiveAtEntrySets previousLiveAtEntry = liveAtEntrySets.put(block, liveAtEntry);
       // If the live-at-entry set changed, add the predecessors to the worklist if they are not
       // already there.
-      if (previousLiveAtEntry == null || !previousLiveAtEntry.equals(live)) {
+      if (previousLiveAtEntry == null || !previousLiveAtEntry.equals(liveAtEntry)) {
         for (BasicBlock pred : block.getPredecessors()) {
           if (!worklist.contains(pred)) {
             worklist.add(pred);
@@ -123,8 +173,9 @@
         }
       }
     }
-    assert liveAtEntrySets.get(sorted.get(0)).size() == 0
-        : "Unexpected values live at entry to first block: " + liveAtEntrySets.get(sorted.get(0));
+    assert liveAtEntrySets.get(sorted.get(0)).isEmpty()
+        : "Unexpected values live at entry to first block: "
+        + liveAtEntrySets.get(sorted.get(0)).liveValues;
     return liveAtEntrySets;
   }
 
@@ -495,9 +546,14 @@
   }
 
   public boolean consistentBlockNumbering() {
-    return blocks.stream()
+    blocks
+        .stream()
         .collect(Collectors.groupingBy(BasicBlock::getNumber, Collectors.counting()))
-        .entrySet().stream().noneMatch((bb2count) -> bb2count.getValue() > 1);
+        .forEach(
+            (key, value) -> {
+              assert value == 1;
+            });
+    return true;
   }
 
   private boolean consistentBlockInstructions() {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index 7892afb..5f62c6c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -366,46 +366,116 @@
   }
 
   @Override
+  public void buildBlockTransfer(
+      IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+    if (predecessorOffset == IRBuilder.INITIAL_BLOCK_OFFSET) {
+      return;
+    }
+    if (currentInstructionIndex != predecessorOffset) {
+      // If transfer is not still in the same block, then update the state to that of the successor.
+      // The builder's lookup of local variables relies on this state for starting locals.
+      currentInstructionIndex = successorOffset;
+      state.reset(incomingState.get(currentInstructionIndex), false);
+      setLocalVariableLists();
+      // The transfer has not yet taken place, so the current position is that of the predecessor.
+      int positionOffset = predecessorOffset;
+      List<CfInstruction> instructions = code.getInstructions();
+      CfInstruction instruction = instructions.get(positionOffset);
+      while (0 < positionOffset && !(instruction instanceof CfPosition)) {
+        instruction = instructions.get(--positionOffset);
+      }
+      if (instruction instanceof CfPosition) {
+        CfPosition position = (CfPosition) instruction;
+        state.setPosition(getCanonicalPosition(position.getPosition()));
+      } else {
+        state.setPosition(canonicalPositions.getPreamblePosition());
+      }
+    }
+    // Manually compute the local variable change for the block transfer.
+    LocalVariableList atSource = getLocalVariables(predecessorOffset);
+    LocalVariableList atTarget = getLocalVariables(successorOffset);
+    if (!isExceptional) {
+      for (Entry<DebugLocalInfo> entry : atSource.locals.int2ObjectEntrySet()) {
+        if (atTarget.locals.get(entry.getIntKey()) != entry.getValue()) {
+          builder.addDebugLocalEnd(entry.getIntKey(), entry.getValue());
+        }
+      }
+    }
+    for (Entry<DebugLocalInfo> entry : atTarget.locals.int2ObjectEntrySet()) {
+      if (atSource.locals.get(entry.getIntKey()) != entry.getValue()) {
+        builder.addDebugLocalStart(entry.getIntKey(), entry.getValue());
+      }
+    }
+  }
+
+  @Override
   public void buildInstruction(
       IRBuilder builder, int instructionIndex, boolean firstBlockInstruction) {
     CfInstruction instruction = code.getInstructions().get(instructionIndex);
     currentInstructionIndex = instructionIndex;
     if (firstBlockInstruction) {
       currentBlockInfo = builder.getCFG().get(instructionIndex);
+      if (instructionIndex == 0 && currentBlockInfo == null) {
+        // If the entry block is also a target the actual entry block is at offset -1.
+        currentBlockInfo = builder.getCFG().get(IRBuilder.INITIAL_BLOCK_OFFSET);
+      }
       state.reset(incomingState.get(instructionIndex), instructionIndex == 0);
     }
+    assert currentBlockInfo != null;
     setLocalVariableLists();
-    readEndingLocals(builder);
-    if (currentBlockInfo != null && instruction.canThrow()) {
+
+    if (instruction.canThrow()) {
       Snapshot exceptionTransfer =
           state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
       for (int target : currentBlockInfo.exceptionalSuccessors) {
         recordStateForTarget(target, exceptionTransfer);
       }
     }
-    if (isControlFlow(instruction)) {
-      ensureDebugValueLivenessControl(builder);
-      instruction.buildIR(builder, state, this);
-      Snapshot stateSnapshot = state.getSnapshot();
-      for (int target : getTargets(instructionIndex)) {
-        recordStateForTarget(target, stateSnapshot);
+
+    boolean localsChanged = localsChanged();
+    boolean hasNextInstructionInCurrentBlock =
+        instructionIndex + 1 != instructionCount()
+            && !builder.getCFG().containsKey(instructionIndex + 1);
+
+    if (instruction.isReturn() || instruction instanceof CfThrow) {
+      // Ensure that all live locals are marked as ending on method exits.
+      assert currentBlockInfo.normalSuccessors.isEmpty();
+      if (currentBlockInfo.exceptionalSuccessors.isEmpty()) {
+        incomingLocals.forEach(builder::addDebugLocalEnd);
+      } else if (!incomingLocals.isEmpty()) {
+        // If the throw instruction does not exit the method, we must end (ensure liveness of) all
+        // locals that are not kept live by at least one of the exceptional successors.
+        Int2ReferenceMap<DebugLocalInfo> live = new Int2ReferenceOpenHashMap<>();
+        for (int successorOffset : currentBlockInfo.exceptionalSuccessors) {
+          live.putAll(getLocalVariables(successorOffset).locals);
+        }
+        for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
+          if (live.get(entry.getIntKey()) != entry.getValue()) {
+            builder.addDebugLocalEnd(entry.getIntKey(), entry.getValue());
+          }
+        }
       }
-      state.clear();
-    } else {
-      if (instruction instanceof CfPosition) {
-        CfPosition cfPosition = (CfPosition) instruction;
-        Position position = cfPosition.getPosition();
-        CfPosition newCfPosition =
-            new CfPosition(cfPosition.getLabel(), getCanonicalPosition(position));
-        newCfPosition.buildIR(builder, state, this);
-      } else {
-        instruction.buildIR(builder, state, this);
-      }
-      ensureDebugValueLiveness(builder);
-      if (builder.getCFG().containsKey(currentInstructionIndex + 1)) {
-        recordStateForTarget(currentInstructionIndex + 1, state.getSnapshot());
-      }
+    } else if (localsChanged && hasNextInstructionInCurrentBlock) {
+      endLocals(builder);
     }
+
+    build(instruction, builder);
+    if (!hasNextInstructionInCurrentBlock) {
+      Snapshot stateSnapshot = state.getSnapshot();
+      if (isControlFlow(instruction)) {
+        for (int target : getTargets(instructionIndex)) {
+          recordStateForTarget(target, stateSnapshot);
+        }
+      } else {
+        recordStateForTarget(instructionIndex + 1, stateSnapshot);
+      }
+    } else if (localsChanged) {
+      startLocals(builder);
+    }
+  }
+
+  private void build(CfInstruction instruction, IRBuilder builder) {
+    instruction.buildIR(builder, state, this);
   }
 
   private void recordStateForTarget(int target, Snapshot snapshot) {
@@ -521,13 +591,24 @@
     }
   }
 
-  private void readEndingLocals(IRBuilder builder) {
-    if (!outgoingLocals.equals(incomingLocals)) {
-      // Add reads of locals ending after the current instruction.
-      for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
-        if (!entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
-          builder.addDebugLocalRead(entry.getIntKey(), entry.getValue());
-        }
+  private boolean localsChanged() {
+    return !incomingLocals.equals(outgoingLocals);
+  }
+
+  private void endLocals(IRBuilder builder) {
+    assert localsChanged();
+    for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
+      if (!entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
+        builder.addDebugLocalEnd(entry.getIntKey(), entry.getValue());
+      }
+    }
+  }
+
+  private void startLocals(IRBuilder builder) {
+    assert localsChanged();
+    for (Entry<DebugLocalInfo> entry : outgoingLocals.int2ObjectEntrySet()) {
+      if (!entry.getValue().equals(incomingLocals.get(entry.getIntKey()))) {
+        builder.addDebugLocalStart(entry.getIntKey(), entry.getValue());
       }
     }
   }
@@ -553,40 +634,6 @@
     return result;
   }
 
-  private void ensureDebugValueLiveness(IRBuilder builder) {
-    if (incomingLocals.equals(outgoingLocals)) {
-      return;
-    }
-    for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
-      if (entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
-        continue;
-      }
-      builder.addDebugLocalEnd(entry.getIntKey(), entry.getValue());
-    }
-    for (Entry<DebugLocalInfo> entry : outgoingLocals.int2ObjectEntrySet()) {
-      if (entry.getValue().equals(incomingLocals.get(entry.getIntKey()))) {
-        continue;
-      }
-      builder.addDebugLocalStart(entry.getIntKey(), entry.getValue());
-    }
-  }
-
-  private void ensureDebugValueLivenessControl(IRBuilder builder) {
-    if (incomingLocals.equals(outgoingLocals)) {
-      return;
-    }
-    for (Entry<DebugLocalInfo> entry : incomingLocals.int2ObjectEntrySet()) {
-      if (entry.getValue().equals(outgoingLocals.get(entry.getIntKey()))) {
-        continue;
-      }
-      builder.addDebugLocalRead(entry.getIntKey(), entry.getValue());
-    }
-    assert outgoingLocals
-        .int2ObjectEntrySet()
-        .stream()
-        .allMatch(entry -> entry.getValue().equals(incomingLocals.get(entry.getIntKey())));
-  }
-
   private boolean isControlFlow(CfInstruction currentInstruction) {
     return currentInstruction.isReturn()
         || currentInstruction.getTarget() != null
@@ -641,7 +688,7 @@
     return state.getPosition();
   }
 
-  private Position getCanonicalPosition(Position position) {
+  public Position getCanonicalPosition(Position position) {
     return canonicalPositions.getCanonical(
         new Position(
             position.line,
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 94516a2..81de890 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
@@ -164,6 +164,12 @@
   }
 
   @Override
+  public void buildBlockTransfer(
+      IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+    // Intentionally empty. Dex front-end does not support debug locals so no transfer info needed.
+  }
+
+  @Override
   public void buildInstruction(
       IRBuilder builder, int instructionIndex, boolean firstBlockInstruction) {
     updateCurrentCatchHandlers(instructionIndex);
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 3612944..05553c1 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
@@ -54,6 +54,7 @@
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.code.InvokeCustom;
+import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.MemberType;
 import com.android.tools.r8.ir.code.Monitor;
 import com.android.tools.r8.ir.code.MoveException;
@@ -137,18 +138,25 @@
   }
 
   private static class MoveExceptionWorklistItem extends WorklistItem {
+    private final int sourceOffset;
     private final int targetOffset;
 
-    private MoveExceptionWorklistItem(BasicBlock block, int targetOffset) {
+    private MoveExceptionWorklistItem(BasicBlock block, int sourceOffset, int targetOffset) {
       super(block, -1);
+      this.sourceOffset = sourceOffset;
       this.targetOffset = targetOffset;
     }
   }
 
   private static class SplitBlockWorklistItem extends WorklistItem {
+    private final int sourceOffset;
+    private final int targetOffset;
 
-    public SplitBlockWorklistItem(BasicBlock block) {
-      super(block, -1);
+    public SplitBlockWorklistItem(
+        int firstInstructionIndex, BasicBlock block, int sourceOffset, int targetOffset) {
+      super(block, firstInstructionIndex);
+      this.sourceOffset = sourceOffset;
+      this.targetOffset = targetOffset;
     }
   }
 
@@ -234,8 +242,9 @@
       return all;
     }
 
-    boolean hasJustOneNormalExit() {
-      return normalSuccessors.size() == 1 && exceptionalSuccessors.isEmpty();
+    boolean hasMoreThanASingleNormalExit() {
+      return normalSuccessors.size() > 1
+          || (normalSuccessors.size() == 1 && !exceptionalSuccessors.isEmpty());
     }
 
     BlockInfo split(
@@ -312,6 +321,8 @@
 
   private BasicBlock entryBlock = null;
   private BasicBlock currentBlock = null;
+  private int currentInstructionOffset = -1;
+
   final private ValueNumberGenerator valueNumberGenerator;
   private final DexEncodedMethod method;
   private final AppInfo appInfo;
@@ -324,7 +335,7 @@
 
   // Pending local reads.
   private Value previousLocalValue = null;
-  private final List<Value> debugLocalReads = new ArrayList<>();
+  private final List<Value> debugLocalEnds = new ArrayList<>();
 
   // Lazily populated list of local values that are referenced without being actually defined.
   private Int2ReferenceMap<List<Value>> uninitializedDebugLocalValues = null;
@@ -580,6 +591,7 @@
       if (item.block.isFilled()) {
         continue;
       }
+      assert debugLocalEnds.isEmpty();
       setCurrentBlock(item.block);
       blocks.add(currentBlock);
       currentBlock.setNumber(nextBlockNumber++);
@@ -589,10 +601,21 @@
         closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
         continue;
       }
-      // Split blocks are just pending close.
+      // Process split blocks which need to emit the locals transfer.
       if (item instanceof SplitBlockWorklistItem) {
-        closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
-        continue;
+        SplitBlockWorklistItem splitEdgeItem = (SplitBlockWorklistItem) item;
+        source.buildBlockTransfer(
+            this, splitEdgeItem.sourceOffset, splitEdgeItem.targetOffset, false);
+        if (item.firstInstructionIndex == -1) {
+          // If the block is a pure split-edge block emit goto (picks up local ends) and close.
+          addInstruction(new Goto(), Position.none());
+          closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
+          continue;
+        } else if (!debugLocalEnds.isEmpty()) {
+          // Otherwise, if some locals ended, insert a read so it takes place at the
+          // predecessor position.
+          addInstruction(new DebugLocalRead());
+        }
       }
       // Build IR for each dex instruction in the block.
       int instCount = source.instructionCount();
@@ -600,12 +623,14 @@
         if (currentBlock == null) {
           break;
         }
-        BlockInfo info = targets.get(source.instructionOffset(i));
+        int instructionOffset = source.instructionOffset(i);
+        BlockInfo info = targets.get(instructionOffset);
         if (info != null && info.block != currentBlock) {
-          closeCurrentBlockWithFallThrough(info.block);
           addToWorklist(info.block, i);
+          closeCurrentBlockWithFallThrough(info.block);
           break;
         }
+        currentInstructionOffset = instructionOffset;
         source.buildInstruction(this, i, i == item.firstInstructionIndex);
       }
     }
@@ -615,17 +640,29 @@
     // TODO(zerny): Link with outer try-block handlers, if any. b/65203529
     int targetIndex = source.instructionIndex(moveExceptionItem.targetOffset);
     int moveExceptionDest = source.getMoveExceptionRegister(targetIndex);
+    Position position = source.getCanonicalDebugPositionAtOffset(moveExceptionItem.targetOffset);
     if (moveExceptionDest >= 0) {
       Value out = writeRegister(moveExceptionDest, ValueType.OBJECT, ThrowingInfo.NO_THROW, null);
-      Position position = source.getCanonicalDebugPositionAtOffset(moveExceptionItem.targetOffset);
       MoveException moveException = new MoveException(out);
       moveException.setPosition(position);
       currentBlock.add(moveException);
     }
-    Goto exit = new Goto();
-    currentBlock.add(exit);
+    // The block-transfer for exceptional edges needs to inform that this is an exceptional transfer
+    // so that local ends become implicit. The reason for this issue is that the "split block" for
+    // and exceptional edge is *after* control transfer, so inserting an end will end up causing
+    // locals to remain live longer than they should. The problem with this is that it is now
+    // possible to resurrect a local by declaring debug info that does not contain the exception
+    // handler but then loading the value from the local index. This should not be a problem in
+    // practice since the stack is empty so the known case of extending the local liveness via the
+    // stack can't happen. If this does end up being an issue, it can potentially be solved by
+    // ending any local that could possibly end in any of the exceptional targets and then
+    // explicitly restart the local on each split-edge that does not end the local.
+    boolean isExceptionalEdge = true;
+    source.buildBlockTransfer(
+        this, moveExceptionItem.sourceOffset, moveExceptionItem.targetOffset, isExceptionalEdge);
     BasicBlock targetBlock = getTarget(moveExceptionItem.targetOffset);
     currentBlock.link(targetBlock);
+    addInstruction(new Goto(), position);
     addToWorklist(targetBlock, targetIndex);
   }
 
@@ -680,7 +717,7 @@
   private Value getIncomingLocalValue(int register, DebugLocalInfo local) {
     assert options.debug;
     assert local != null;
-    assert local == getIncomingLocal(register);
+    // TODO(b/111251032): Here we lookup a value with type based on debug info. That's just wrong!
     ValueType valueType = ValueType.fromDexType(local.type);
     return readRegisterIgnoreLocal(register, valueType, local);
   }
@@ -691,47 +728,21 @@
     return !value.isUninitializedLocal() && value.getLocalInfo() == local;
   }
 
-  public void addDebugLocalRead(int register, DebugLocalInfo local) {
-    if (!options.debug) {
-      return;
-    }
-    Value value = getIncomingLocalValue(register, local);
-    if (isValidFor(value, local)) {
-      debugLocalReads.add(value);
-    }
-  }
-
   public void addDebugLocalStart(int register, DebugLocalInfo local) {
     if (!options.debug) {
       return;
     }
     assert local != null;
     assert local == getOutgoingLocal(register);
+    // TODO(b/111251032): Here we lookup a value with type based on debug info. That's just wrong!
     ValueType valueType = ValueType.fromDexType(local.type);
-    // TODO(mathiasr): Here we create a Phi with type based on debug info. That's just wrong!
     Value incomingValue = readRegisterIgnoreLocal(register, valueType, local);
-
-    // TODO(mathiasr): This can be simplified once trivial phi removal is local-info aware.
-    if (incomingValue.isPhi() || incomingValue.getLocalInfo() != local) {
+    // If the local was not introduced by the previous instruction, start it here.
+    if (incomingValue.getLocalInfo() != local
+        || currentBlock.isEmpty()
+        || currentBlock.getInstructions().getLast().outValue() != incomingValue) {
       addDebugLocalWrite(ValueType.fromDexType(local.type), register, incomingValue);
-      return;
     }
-    assert incomingValue.getLocalInfo() == local;
-    assert !incomingValue.isUninitializedLocal();
-
-    // 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 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();
-    if (instruction.outValue() == incomingValue) {
-      return;
-    }
-    instruction.addDebugValue(incomingValue);
-    incomingValue.addDebugLocalStart(instruction);
   }
 
   public void addDebugLocalEnd(int register, DebugLocalInfo local) {
@@ -739,38 +750,18 @@
       return;
     }
     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.
-    // 3. The block has an instruction defining the local being ended.
-    if (currentBlock.getInstructions().isEmpty()) {
-      addInstruction(new DebugLocalRead());
-    }
-    Instruction instruction = currentBlock.getInstructions().getLast();
-    if (instruction.outValue() != value) {
-      instruction.addDebugValue(value);
-      value.addDebugLocalEnd(instruction);
-      return;
-    }
-    // In case 3. there are two cases:
-    // a. The defining instruction is a debug-write, in which case it should be removed.
-    // b. The defining instruction is overwriting the local value, in which case we de-associate it.
-    assert !instruction.outValue().isUsed();
-    if (instruction.isDebugLocalWrite()) {
-      DebugLocalWrite write = instruction.asDebugLocalWrite();
-      currentBlock.replaceCurrentDefinitions(value, write.src());
-      currentBlock.listIterator(write).removeOrReplaceByDebugLocalRead();
-    } else {
-      instruction.outValue().clearLocalInfo();
+    if (isValidFor(value, local)) {
+      debugLocalEnds.add(value);
     }
   }
 
   public void addDebugPosition(Position position) {
     if (options.debug) {
       assert source.getCurrentPosition().equals(position);
+      if (!debugLocalEnds.isEmpty()) {
+        // If there are pending local ends, end them before changing the line.
+        addInstruction(new DebugLocalRead(), Position.none());
+      }
       addInstruction(new DebugPosition());
     }
   }
@@ -1007,12 +998,11 @@
   }
 
   public void addGoto(int targetOffset) {
-    addInstruction(new Goto());
     BasicBlock targetBlock = getTarget(targetOffset);
     assert !currentBlock.hasCatchSuccessor(targetBlock);
     currentBlock.link(targetBlock);
     addToWorklist(targetBlock, source.instructionIndex(targetOffset));
-    closeCurrentBlock();
+    closeCurrentBlock(new Goto());
   }
 
   private void addTrivialIf(int trueTargetOffset, int falseTargetOffset) {
@@ -1024,14 +1014,12 @@
     // We expected an if here and therefore we incremented the expected predecessor count
     // twice for the following block.
     target.decrementUnfilledPredecessorCount();
-    addInstruction(new Goto());
     currentBlock.link(target);
     addToWorklist(target, source.instructionIndex(trueTargetOffset));
-    closeCurrentBlock();
+    closeCurrentBlock(new Goto());
   }
 
   private void addNonTrivialIf(If instruction, int trueTargetOffset, int falseTargetOffset) {
-    addInstruction(instruction);
     BasicBlock trueTarget = getTarget(trueTargetOffset);
     BasicBlock falseTarget = getTarget(falseTargetOffset);
     currentBlock.link(trueTarget);
@@ -1039,7 +1027,7 @@
     // Generate fall-through before the block that is branched to.
     addToWorklist(falseTarget, source.instructionIndex(falseTargetOffset));
     addToWorklist(trueTarget, source.instructionIndex(trueTargetOffset));
-    closeCurrentBlock();
+    closeCurrentBlock(instruction);
   }
 
   public void addIf(If.Type type, ValueType operandType, int value1, int value2,
@@ -1406,8 +1394,7 @@
     // Attach the live locals to the return instruction to avoid a local change on monitor exit.
     attachLocalValues(ret);
     source.buildPostlude(this);
-    addInstruction(ret);
-    closeCurrentBlock();
+    closeCurrentBlock(ret);
   }
 
   public void addStaticGet(int dest, DexField field) {
@@ -1497,8 +1484,8 @@
     // Create a switch with only the non-fallthrough targets.
     keys = nonFallthroughKeys.toIntArray();
     labelOffsets = nonFallthroughOffsets.toIntArray();
-    addInstruction(createSwitch(switchValue, keys, fallthroughOffset, labelOffsets));
-    closeCurrentBlock();
+    Switch aSwitch = createSwitch(switchValue, keys, fallthroughOffset, labelOffsets);
+    closeCurrentBlock(aSwitch);
   }
 
   private Switch createSwitch(Value value, int[] keys, int fallthroughOffset, int[] targetOffsets) {
@@ -1537,6 +1524,9 @@
 
   public void addThrow(int value) {
     Value in = readRegister(value, ValueType.OBJECT);
+    // The only successors to a throw instruction are exceptional, so we directly add it (ensuring
+    // the exceptional edges which are split-edge by construction) and then we close the block which
+    // cannot have any additional edges that need splitting.
     addInstruction(new Throw(in));
     closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
   }
@@ -1932,7 +1922,8 @@
             header = new BasicBlock();
             header.incrementUnfilledPredecessorCount();
             moveExceptionHeaders.put(target, header);
-            ssaWorklist.add(new MoveExceptionWorklistItem(header, targetOffset));
+            ssaWorklist.add(
+                new MoveExceptionWorklistItem(header, currentInstructionOffset, targetOffset));
           }
           targets.add(header);
         }
@@ -1944,20 +1935,22 @@
   private void attachLocalValues(Instruction ir) {
     if (!options.debug) {
       assert previousLocalValue == null;
-      assert debugLocalReads.isEmpty();
+      assert debugLocalEnds.isEmpty();
       return;
     }
     // Add a use if this instruction is overwriting a previous value of the same local.
     if (previousLocalValue != null && previousLocalValue.getLocalInfo() == ir.getLocalInfo()) {
       assert ir.outValue() != null;
       ir.addDebugValue(previousLocalValue);
+      previousLocalValue.addDebugLocalEnd(ir);
     }
     // Add reads of locals if any are pending.
-    for (Value value : debugLocalReads) {
+    for (Value value : debugLocalEnds) {
       ir.addDebugValue(value);
+      value.addDebugLocalEnd(ir);
     }
     previousLocalValue = null;
-    debugLocalReads.clear();
+    debugLocalEnds.clear();
   }
 
   // Package (ie, SourceCode accessed) helpers.
@@ -2067,34 +2060,74 @@
   }
 
   private void closeCurrentBlockGuaranteedNotToNeedEdgeSplitting() {
-    // TODO(zerny): To ensure liveness of locals throughout the entire block, we might want to
-    // insert reads before closing the block. It is unclear if we can rely on a local-end to ensure
-    // liveness in all blocks where the local should be live.
     assert currentBlock != null;
     currentBlock.close(this);
     setCurrentBlock(null);
     throwingInstructionInCurrentBlock = false;
+    currentInstructionOffset = -1;
+    assert debugLocalEnds.isEmpty();
   }
 
-  private void closeCurrentBlock() {
+  private void closeCurrentBlock(JumpInstruction jumpInstruction) {
+    assert !jumpInstruction.instructionTypeCanThrow();
     assert currentBlock != null;
+    assert currentBlock.getInstructions().isEmpty()
+        || !currentBlock.getInstructions().getLast().isJumpInstruction();
+    generateSplitEdgeBlocks();
+    addInstruction(jumpInstruction);
+    closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
+  }
+
+  private void closeCurrentBlockWithFallThrough(BasicBlock nextBlock) {
+    assert currentBlock != null;
+    assert !currentBlock.hasCatchSuccessor(nextBlock);
+    currentBlock.link(nextBlock);
+    closeCurrentBlock(new Goto());
+  }
+
+  private void generateSplitEdgeBlocks() {
+    assert currentBlock != null;
+    assert currentBlock.isEmpty() || !currentBlock.getInstructions().getLast().isJumpInstruction();
     BlockInfo info = getBlockInfo(currentBlock);
-    if (!info.hasJustOneNormalExit()) {
+    if (info.hasMoreThanASingleNormalExit()) {
       // Exceptional edges are always split on construction, so no need to split edges to them.
+      // Introduce split-edge blocks for all normal edges and push them on the work list.
       for (int successorOffset : info.normalSuccessors) {
         BlockInfo successorInfo = getBlockInfo(successorOffset);
-        if (successorInfo.predecessorCount() > 1) {
+        if (successorInfo.predecessorCount() == 1) {
+          // re-add to worklist as a unique succ
+          WorklistItem oldItem = null;
+          for (WorklistItem item : ssaWorklist) {
+            if (item.block == successorInfo.block) {
+              oldItem = item;
+            }
+          }
+          assert oldItem.firstInstructionIndex == source.instructionIndex(successorOffset);
+          ssaWorklist.remove(oldItem);
+          ssaWorklist.add(
+              new SplitBlockWorklistItem(
+                  oldItem.firstInstructionIndex,
+                  oldItem.block,
+                  currentInstructionOffset,
+                  successorOffset));
+        } else {
           BasicBlock splitBlock = createSplitEdgeBlock(currentBlock, successorInfo.block);
-          ssaWorklist.add(new SplitBlockWorklistItem(splitBlock));
+          ssaWorklist.add(
+              new SplitBlockWorklistItem(
+                  -1, splitBlock, currentInstructionOffset, successorOffset));
         }
       }
+    } else if (info.normalSuccessors.size() == 1) {
+      int successorOffset = info.normalSuccessors.iterator().nextInt();
+      source.buildBlockTransfer(this, currentInstructionOffset, successorOffset, false);
+    } else {
+      // TODO(zerny): Consider refactoring to compute the live-at-exit via callback here too.
+      assert info.allSuccessors().isEmpty();
     }
-    closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
   }
 
   private static BasicBlock createSplitEdgeBlock(BasicBlock source, BasicBlock target) {
     BasicBlock splitBlock = new BasicBlock();
-    splitBlock.add(new Goto());
     splitBlock.incrementUnfilledPredecessorCount();
     splitBlock.getPredecessors().add(source);
     splitBlock.getSuccessors().add(target);
@@ -2103,14 +2136,6 @@
     return splitBlock;
   }
 
-  private void closeCurrentBlockWithFallThrough(BasicBlock nextBlock) {
-    assert currentBlock != null;
-    addInstruction(new Goto());
-    assert !currentBlock.hasCatchSuccessor(nextBlock);
-    currentBlock.link(nextBlock);
-    closeCurrentBlock();
-  }
-
   /**
    * Change to control-flow graph to avoid repeated phi operands when all the same values
    * flow in from multiple predecessors.
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 01c4dd0..e528a90 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
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRBuilder.BlockInfo;
 import com.android.tools.r8.ir.conversion.JarState.Local;
+import com.android.tools.r8.ir.conversion.JarState.LocalChangeAtOffset;
 import com.android.tools.r8.ir.conversion.JarState.Slot;
 import com.android.tools.r8.logging.Log;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -467,6 +468,32 @@
   }
 
   @Override
+  public void buildBlockTransfer(
+      IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+    assert currentInstruction == null || predecessorOffset == getOffset(currentInstruction);
+    currentInstruction = null;
+    if (predecessorOffset == IRBuilder.INITIAL_BLOCK_OFFSET
+        || successorOffset == EXCEPTIONAL_SYNC_EXIT_OFFSET) {
+      return;
+    }
+    currentPosition = getCanonicalDebugPositionAtOffset(predecessorOffset);
+
+    LocalChangeAtOffset localChange = state.getLocalChange(predecessorOffset, successorOffset);
+    if (!isExceptional) {
+      for (Local toClose : localChange.getLocalsToClose()) {
+        builder.addDebugLocalEnd(toClose.slot.register, toClose.info);
+      }
+    }
+    List<Local> localsToOpen = localChange.getLocalsToOpen();
+    if (!localsToOpen.isEmpty()) {
+      state.restoreState(successorOffset);
+      for (Local toOpen : localsToOpen) {
+        builder.addDebugLocalStart(toOpen.slot.register, toOpen.info);
+      }
+    }
+  }
+
+  @Override
   public void buildInstruction(
       IRBuilder builder, int instructionIndex, boolean firstBlockInstruction) {
     if (instructionIndex == EXCEPTIONAL_SYNC_EXIT_OFFSET) {
@@ -482,13 +509,7 @@
     // current position will be updated by LineNumberNode into this block.
     if (firstBlockInstruction || instructionIndex == 0) {
       state.restoreState(instructionIndex);
-      // Don't include line changes when processing a label. Doing so will end up emitting local
-      // writes after the line has changed and thus causing locals to become visible too late.
-      currentPosition =
-          getCanonicalDebugPositionAtOffset(
-              ((instructionIndex > 0) && (insn instanceof LabelNode))
-                  ? instructionIndex - 1
-                  : instructionIndex);
+      currentPosition = getCanonicalDebugPositionAtOffset(instructionIndex);
     }
 
     String preInstructionState;
@@ -496,30 +517,20 @@
       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.
+    if (hasNextInstruction) {
+      // Explicitly end all locals ending at this point.
       for (Local local : state.getLocalsToClose()) {
         builder.addDebugLocalEnd(local.slot.register, local.info);
       }
     }
+    build(insn, builder);
+    // If the block continues past this instruction then local state should be updated.
     if (hasNextInstruction) {
-      // Open the scope of locals starting at this point.
+      // Ensure starts of locals starting at this point.
       for (Local local : state.getLocalsToOpen()) {
         builder.addDebugLocalStart(local.slot.register, local.info);
       }
@@ -536,6 +547,8 @@
             offset, instructionToString(insn), preInstructionState, state);
       }
     }
+
+    currentInstruction = null;
   }
 
   private boolean verifyExceptionEdgesAreRecorded(AbstractInsnNode insn) {
@@ -1862,23 +1875,12 @@
     }
   }
 
-  private void processLocalVariablesAtControlEdge(AbstractInsnNode insn, IRBuilder builder) {
-    assert isControlFlowInstruction(insn) && !isReturn(insn);
-    int offset = getOffset(insn);
-    int blockOffset = builder.getCFG().headMap(offset).lastIntKey();
-    BlockInfo blockInfo = builder.getCFG().get(blockOffset);
-    // Read all locals that are not live on all successors to ensure liveness.
-    for (Local local : state.localsNotLiveAtAllSuccessors(blockInfo.allSuccessors())) {
-      builder.addDebugLocalRead(local.slot.register, local.info);
-    }
-  }
-
   private void processLocalVariablesAtExit(AbstractInsnNode insn, IRBuilder builder) {
     assert isReturn(insn) || isThrow(insn);
     // Read all locals live at exit to ensure liveness.
     for (Local local : state.getLocals()) {
       if (local.info != null) {
-        builder.addDebugLocalRead(local.slot.register, local.info);
+        builder.addDebugLocalEnd(local.slot.register, local.info);
       }
     }
   }
@@ -2323,7 +2325,24 @@
     if (isExitingThrow(insn)) {
       processLocalVariablesAtExit(insn, builder);
     } else {
-      processLocalVariablesAtControlEdge(insn, builder);
+      int offset = getOffset(insn);
+      Int2ReferenceSortedMap<BlockInfo> cfg = builder.getCFG();
+      BlockInfo info = cfg.get(cfg.headMap(offset + 1).lastIntKey());
+      assert info.normalSuccessors.isEmpty();
+      assert !info.exceptionalSuccessors.isEmpty();
+      Int2ReferenceMap<DebugLocalInfo> ending = new Int2ReferenceOpenHashMap<>();
+      for (int successorOffset : info.exceptionalSuccessors) {
+        if (successorOffset == EXCEPTIONAL_SYNC_EXIT_OFFSET) {
+          // TODO(zerny): It would likely be beneficial to keep locals live until the exit from the
+          // exceptional sync exit block.
+          continue;
+        }
+        LocalChangeAtOffset localChange = state.getLocalChange(offset, successorOffset);
+        for (Local localEnd : localChange.getLocalsToClose()) {
+          ending.put(localEnd.slot.register, localEnd.info);
+        }
+      }
+      ending.forEach(builder::addDebugLocalEnd);
     }
     builder.addThrow(register);
   }
@@ -2659,7 +2678,6 @@
   }
 
   private void build(JumpInsnNode insn, IRBuilder builder) {
-    processLocalVariablesAtControlEdge(insn, builder);
     int[] targets = getTargets(insn);
     int opcode = insn.getOpcode();
     if (Opcodes.IFEQ <= opcode && opcode <= Opcodes.IF_ACMPNE) {
@@ -2749,12 +2767,10 @@
   }
 
   private void build(TableSwitchInsnNode insn, IRBuilder builder) {
-    processLocalVariablesAtControlEdge(insn, builder);
     buildSwitch(insn.dflt, insn.labels, new int[]{insn.min}, builder);
   }
 
   private void build(LookupSwitchInsnNode insn, IRBuilder builder) {
-    processLocalVariablesAtControlEdge(insn, builder);
     int[] keys = new int[insn.keys.size()];
     for (int i = 0; i < insn.keys.size(); i++) {
       keys[i] = (int) insn.keys.get(i);
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 b67d12e..062905a 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
@@ -81,7 +81,7 @@
   }
 
   // Collection of locals information at a program point.
-  private static class LocalsAtOffset {
+  static class LocalsAtOffset {
     // Note that we assume live is always a super-set of starts.
     final List<LocalNodeInfo> live;
     final List<LocalNodeInfo> starts;
@@ -259,6 +259,41 @@
     }
   }
 
+  public static class LocalChangeAtOffset {
+
+    final LocalsAtOffset atExit;
+    final LocalsAtOffset atEntry;
+    private JarState state;
+
+    private LocalChangeAtOffset(LocalsAtOffset atExit, LocalsAtOffset atEntry, JarState state) {
+      this.atExit = atExit;
+      this.atEntry = atEntry;
+      this.state = state;
+    }
+
+    public List<Local> getLocalsToClose() {
+      List<Local> toClose = new ArrayList<>(atExit.live.size());
+      for (LocalNodeInfo liveAtExit : atExit.live) {
+        if (!atEntry.isLive(liveAtExit.info)) {
+          int register = state.getLocalRegister(liveAtExit.node.index, liveAtExit.type);
+          toClose.add(new Local(new Slot(register, liveAtExit.type), liveAtExit.info));
+        }
+      }
+      return toClose;
+    }
+
+    public List<Local> getLocalsToOpen() {
+      List<Local> toOpen = new ArrayList<>(atEntry.live.size());
+      for (LocalNodeInfo liveAtEntry : atEntry.live) {
+        if (!atExit.isLive(liveAtEntry.info)) {
+          int register = state.getLocalRegister(liveAtEntry.node.index, liveAtEntry.type);
+          toOpen.add(new Local(new Slot(register, liveAtEntry.type), liveAtEntry.info));
+        }
+      }
+      return toOpen;
+    }
+  }
+
   final int startOfStack;
   private int topOfStack;
 
@@ -529,6 +564,18 @@
     localsToClose.clear();
   }
 
+  private LocalsAtOffset getLocalsAtOffset(int offset) {
+    Int2ReferenceSortedMap<LocalsAtOffset> headMap = localsAtOffsetTable.headMap(offset + 1);
+    return localsAtOffsetTable.get(headMap.lastIntKey());
+  }
+
+  public LocalChangeAtOffset getLocalChange(int predecessorIndex, int successorIndex) {
+    return new LocalChangeAtOffset(
+        getLocalsAtOffset(predecessorIndex),
+        getLocalsAtOffset(successorIndex),
+        this);
+  }
+
   public List<Local> getLocalsToClose() {
     return localsToClose;
   }
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 2bc2aa1..9b5dc20 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
@@ -49,6 +49,9 @@
 
   void buildInstruction(IRBuilder builder, int instructionIndex, boolean firstBlockInstruction);
 
+  void buildBlockTransfer(
+      IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional);
+
   void buildPostlude(IRBuilder builder);
 
   // Helper to resolve switch payloads and build switch instructions (dex code only).
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 df6e65a..b856cc5 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
@@ -2221,6 +2221,7 @@
             Value overwrittenLocal = instruction.removeDebugValue(localInfo);
             if (overwrittenLocal != null) {
               inValue.definition.addDebugValue(overwrittenLocal);
+              overwrittenLocal.addDebugLocalEnd(inValue.definition);
             }
             if (prevInstruction != null) {
               instruction.moveDebugValues(prevInstruction);
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 5a7cba6..3e3413a 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
@@ -1022,6 +1022,12 @@
     }
 
     @Override
+    public void buildBlockTransfer(
+        IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+      throw new Unreachable("Outliner does not support control flow");
+    }
+
+    @Override
     public void buildPostlude(IRBuilder builder) {
       // Intentionally left empty. (Needed for Java-bytecode-frontend synchronization support.)
     }
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 8fd09d0..0d93eee 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
@@ -17,6 +17,7 @@
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRCode.LiveAtEntrySets;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
@@ -31,7 +32,6 @@
 import com.android.tools.r8.ir.regalloc.RegisterPositions.Type;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableList;
@@ -126,7 +126,7 @@
   private final InternalOptions options;
 
   // Mapping from basic blocks to the set of values live at entry to that basic block.
-  private Map<BasicBlock, Set<Value>> liveAtEntrySets;
+  private Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets;
   // The value of the first argument, or null if the method has no arguments.
   protected Value firstArgumentValue;
   // The value of the last argument, or null if the method has no arguments.
@@ -254,12 +254,14 @@
   }
 
   private void computeDebugInfo(ImmutableList<BasicBlock> blocks) {
-    computeDebugInfo(blocks, liveIntervals, this);
+    computeDebugInfo(blocks, liveIntervals, this, liveAtEntrySets);
   }
 
   public static void computeDebugInfo(
-      ImmutableList<BasicBlock> blocks, List<LiveIntervals> liveIntervals,
-      RegisterAllocator allocator) {
+      ImmutableList<BasicBlock> blocks,
+      List<LiveIntervals> liveIntervals,
+      RegisterAllocator allocator,
+      Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets) {
     // Collect live-ranges for all SSA values with local information.
     List<LocalRange> ranges = new ArrayList<>();
     for (LiveIntervals interval : liveIntervals) {
@@ -267,46 +269,19 @@
       if (!value.hasLocalInfo()) {
         continue;
       }
-      List<Integer> starts = ListUtils.map(value.getDebugLocalStarts(), Instruction::getNumber);
-      List<Integer> ends = ListUtils.map(value.getDebugLocalEnds(), Instruction::getNumber);
-      List<LiveRange> liveRanges = new ArrayList<>();
-      liveRanges.addAll(interval.getRanges());
+      List<LiveRange> liveRanges = new ArrayList<>(interval.getRanges());
       for (LiveIntervals child : interval.getSplitChildren()) {
         assert child.getValue() == value;
         assert child.getSplitChildren() == null || child.getSplitChildren().isEmpty();
         liveRanges.addAll(child.getRanges());
       }
       liveRanges.sort((r1, r2) -> Integer.compare(r1.start, r2.start));
-      starts.sort(Integer::compare);
-      ends.sort(Integer::compare);
-
       for (LiveRange liveRange : liveRanges) {
         int start = liveRange.start;
         int end = liveRange.end;
-        Integer nextEnd;
-        while ((nextEnd = nextInRange(start, end, ends)) != null) {
-          // If an argument value has been split, we have disallowed argument reuse and therefore,
-          // the argument value is also in the argument register throughout the method. For debug
-          // information, we always use the argument register whenever a local corresponds to an
-          // argument value. That avoids ending and restarting locals whenever we move arguments
-          // to lower register.
-          int register = allocator.getArgumentOrAllocateRegisterForValue(value, start);
-          ranges.add(new LocalRange(value, register, start, nextEnd));
-          Integer nextStart = nextInRange(nextEnd, end, starts);
-          if (nextStart == null) {
-            start = -1;
-            break;
-          }
-          start = nextStart;
-        }
-        if (start >= 0) {
-          ranges.add(
-              new LocalRange(
-                  value,
-                  allocator.getArgumentOrAllocateRegisterForValue(value, start),
-                  start,
-                  end));
-        }
+        ranges.add(
+            new LocalRange(
+                value, allocator.getArgumentOrAllocateRegisterForValue(value, start), start, end));
       }
     }
     if (ranges.isEmpty()) {
@@ -321,11 +296,33 @@
     Int2ReferenceMap<DebugLocalInfo> ending = new Int2ReferenceOpenHashMap<>();
     Int2ReferenceMap<DebugLocalInfo> starting = new Int2ReferenceOpenHashMap<>();
 
+    boolean isEntryBlock = true;
     for (BasicBlock block : blocks) {
-      // Skip past all spill moves to obtain the instruction number of the actual first instruction.
       InstructionListIterator instructionIterator = block.listIterator();
-      instructionIterator.nextUntil(
-          i -> !i.isArgument() && !i.isMoveException() && !isSpillInstruction(i));
+      Set<Value> liveLocalValues = new HashSet<>(liveAtEntrySets.get(block).liveLocalValues);
+      // Skip past arguments and open argument and phi locals.
+      if (isEntryBlock) {
+        isEntryBlock = false;
+        assert block.getPhis().isEmpty();
+        while (instructionIterator.hasNext()) {
+          Instruction instruction = instructionIterator.next();
+          if (!instruction.isArgument()) {
+            break;
+          }
+          if (instruction.outValue().hasLocalInfo()) {
+            liveLocalValues.add(instruction.outValue());
+          }
+        }
+        instructionIterator.previous();
+      } else {
+        for (Phi phi : block.getPhis()) {
+          if (phi.hasLocalInfo()) {
+            liveLocalValues.add(phi);
+          }
+        }
+      }
+      // Skip past all spill moves to obtain the instruction number of the actual first instruction.
+      instructionIterator.nextUntil(i -> !i.isMoveException() && !isSpillInstruction(i));
       Instruction firstInstruction = instructionIterator.previous();
       int firstIndex = firstInstruction.getNumber();
 
@@ -333,14 +330,18 @@
       // might be live upon entering the first instruction (if they are used by it). Since we
       // skipped move-exception this closes locals at the move exception which should close as part
       // of the exceptional transfer.
-      openRanges.removeIf(openRange -> !isLocalLiveAtInstruction(firstInstruction, openRange));
+      openRanges.removeIf(
+          openRange ->
+              !liveLocalValues.contains(openRange.value)
+                  || !isLocalLiveAtInstruction(firstInstruction, openRange));
 
       // Open ranges up-to but excluding the first instruction. Starts are inclusive but entry is
       // prior to the first instruction.
       while (nextStartingRange != null && nextStartingRange.start < firstIndex) {
         // If the range is live at this index open it. Again the end is inclusive here because the
         // instruction is live at block entry if it is live at entry to the first instruction.
-        if (isLocalLiveAtInstruction(firstInstruction, nextStartingRange)) {
+        if (liveLocalValues.contains(nextStartingRange.value)
+            && isLocalLiveAtInstruction(firstInstruction, nextStartingRange)) {
           openRanges.add(nextStartingRange);
         }
         nextStartingRange = rangeIterator.hasNext() ? rangeIterator.next() : null;
@@ -359,30 +360,51 @@
       // Iterate the block instructions and emit locals changed events.
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
-        if (instruction.isDebugLocalRead()) {
-          // Remove debug local reads now that local liveness is computed.
-          assert !instruction.getDebugValues().isEmpty();
-          instruction.clearDebugValues();
-          instructionIterator.remove();
-        }
         if (!instructionIterator.hasNext()) {
           break;
         }
+
+        if (!instruction.getDebugValues().isEmpty()) {
+          for (Value endAnnotation : instruction.getDebugValues()) {
+            ListIterator<LocalRange> it = openRanges.listIterator();
+            while (it.hasNext()) {
+              LocalRange openRange = it.next();
+              if (openRange.value == endAnnotation) {
+                it.remove();
+                assert currentLocals.get(openRange.register) == openRange.local;
+                currentLocals.remove(openRange.register);
+                ending.put(openRange.register, openRange.local);
+                break;
+              }
+            }
+          }
+          // Remove the end markers now that local liveness is computed.
+          instruction.clearDebugValues();
+          if (instruction.isDebugLocalRead()) {
+            Instruction prev = instructionIterator.previous();
+            assert prev == instruction;
+            instructionIterator.remove();
+          }
+        }
+
         Instruction nextInstruction = instructionIterator.peekNext();
         if (isSpillInstruction(nextInstruction)) {
           // No need to insert a DebugLocalsChange instruction before a spill instruction.
           continue;
         }
         int index = nextInstruction.getNumber();
-        ListIterator<LocalRange> it = openRanges.listIterator(0);
-        while (it.hasNext()) {
-          LocalRange openRange = it.next();
-          // Close ranges up-to but excluding the first instruction.
-          if (!isLocalLiveAtInstruction(nextInstruction, openRange)) {
-            it.remove();
-            assert currentLocals.get(openRange.register) == openRange.local;
-            currentLocals.remove(openRange.register);
-            ending.put(openRange.register, openRange.local);
+        {
+          ListIterator<LocalRange> it = openRanges.listIterator();
+          while (it.hasNext()) {
+            LocalRange openRange = it.next();
+            // Close ranges up-to but excluding the first instruction.
+            if (!isLocalLiveAtInstruction(nextInstruction, openRange)) {
+              it.remove();
+              // It may be that currentLocals does not contain this local because an explicit end
+              // already closed the local.
+              currentLocals.remove(openRange.register);
+              ending.put(openRange.register, openRange.local);
+            }
           }
         }
         while (nextStartingRange != null && nextStartingRange.start < index) {
@@ -2292,7 +2314,7 @@
         int toInstruction = successor.entry().getNumber();
 
         // Insert spill/restore moves when a value changes across a block boundary.
-        Set<Value> liveAtEntry = liveAtEntrySets.get(successor);
+        Set<Value> liveAtEntry = liveAtEntrySets.get(successor).liveValues;
         for (Value value : liveAtEntry) {
           LiveIntervals parentInterval = value.getLiveIntervals();
           LiveIntervals fromIntervals = parentInterval.getSplitCovering(fromInstruction);
@@ -2388,8 +2410,8 @@
           LiveIntervals thisIntervals = thisValue.getLiveIntervals();
           thisIntervals.getRanges().clear();
           thisIntervals.addRange(new LiveRange(0, code.getNextInstructionNumber()));
-          for (Set<Value> values : liveAtEntrySets.values()) {
-            values.add(thisValue);
+          for (LiveAtEntrySets values : liveAtEntrySets.values()) {
+            values.liveValues.add(thisValue);
           }
           return;
         }
@@ -2397,20 +2419,18 @@
     }
   }
 
-  /**
-   * Compute live ranges based on liveAtEntry sets for all basic blocks.
-   */
+  /** Compute live ranges based on liveAtEntry sets for all basic blocks. */
   public static void computeLiveRanges(
       InternalOptions options,
       IRCode code,
-      Map<BasicBlock, Set<Value>> liveAtEntrySets,
+      Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets,
       List<LiveIntervals> liveIntervals) {
     for (BasicBlock block : code.topologicallySortedBlocks()) {
       Set<Value> live = new HashSet<>();
       List<BasicBlock> successors = block.getSuccessors();
       Set<Value> phiOperands = new HashSet<>();
       for (BasicBlock successor : successors) {
-        live.addAll(liveAtEntrySets.get(successor));
+        live.addAll(liveAtEntrySets.get(successor).liveValues);
         for (Phi phi : successor.getPhis()) {
           live.remove(phi);
           phiOperands.add(phi.getOperand(successor.getPredecessors().indexOf(block)));
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 991d371..3d416fb 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
@@ -192,6 +192,12 @@
   }
 
   @Override
+  public void buildBlockTransfer(
+      IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+    // Intensionally empty as synthetic code does not contain locals information.
+  }
+
+  @Override
   public final void resolveAndBuildSwitch(
       int value, int fallthroughOffset, int payloadOffset, IRBuilder builder) {
     throw new Unreachable("Unexpected call to resolveAndBuildSwitch");
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index e9ef5ed..7911016 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -246,6 +246,9 @@
 
           @Override
           public JUnit3Wrapper.DebuggeeState get() {
+            if (wrapper.state == JUnit3Wrapper.State.Exit) {
+              return null;
+            }
             assert verifyStateLocation(wrapper.getDebuggeeState());
             if (initial) {
               if (DEBUG_TESTS) {
diff --git a/src/test/java/com/android/tools/r8/debug/LocalEndTest.java b/src/test/java/com/android/tools/r8/debug/LocalEndTest.java
new file mode 100644
index 0000000..036e590
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTest.java
@@ -0,0 +1,27 @@
+// 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 LocalEndTest {
+
+  public void foo() {
+    {
+      int x = 42;
+      try {
+        bar();
+        x = 7;
+      } catch (Throwable e) {}
+    }
+    int y = 11; // Replaced by stack value of previously visible x (which must not become visible).
+    System.out.println(y);
+  }
+
+  private void bar() {
+    // nothing to do
+  }
+
+  public static void main(String[] args) {
+    new LocalEndTest().foo();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LocalEndTestDump.java b/src/test/java/com/android/tools/r8/debug/LocalEndTestDump.java
new file mode 100644
index 0000000..ad6737c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTestDump.java
@@ -0,0 +1,148 @@
+// 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.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class LocalEndTestDump implements Opcodes {
+
+  public static byte[] dump() {
+
+    ClassWriter cw = new ClassWriter(0);
+    MethodVisitor mv;
+
+    cw.visit(
+        V1_8,
+        ACC_PUBLIC + ACC_SUPER,
+        "com/android/tools/r8/debug/LocalEndTest",
+        null,
+        "java/lang/Object",
+        null);
+
+    cw.visitSource("LocalEndTest.java", null);
+
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      mv.visitCode();
+      Label l0 = new Label();
+      mv.visitLabel(l0);
+      mv.visitLineNumber(6, l0);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitInsn(RETURN);
+      Label l1 = new Label();
+      mv.visitLabel(l1);
+      mv.visitLocalVariable("this", "Lcom/android/tools/r8/debug/LocalEndTest;", null, l0, l1, 0);
+      mv.visitMaxs(1, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "foo", "()V", null, null);
+      mv.visitCode();
+      Label l0 = new Label();
+      Label l1 = new Label();
+      Label l2 = new Label();
+      mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Throwable");
+      Label l3 = new Label();
+      mv.visitLabel(l3);
+      mv.visitLineNumber(10, l3);
+      mv.visitIntInsn(BIPUSH, 42);
+      mv.visitVarInsn(ISTORE, 1);
+      mv.visitLabel(l0);
+      mv.visitLineNumber(12, l0);
+      mv.visitVarInsn(ILOAD, 1); // push x on the stack for later use in the join block.
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "com/android/tools/r8/debug/LocalEndTest", "bar", "()V", false);
+      Label l4 = new Label();
+      mv.visitLabel(l4);
+      mv.visitLineNumber(13, l4);
+      mv.visitIntInsn(BIPUSH, 7);
+      mv.visitVarInsn(ISTORE, 1);
+      mv.visitLabel(l1);
+      mv.visitLineNumber(14, l1);
+      Label l5 = new Label();
+      mv.visitJumpInsn(GOTO, l5);
+      mv.visitLabel(l2);
+      mv.visitFrame(
+          Opcodes.F_FULL,
+          2,
+          new Object[] {"com/android/tools/r8/debug/LocalEndTest", Opcodes.INTEGER},
+          1,
+          new Object[] {"java/lang/Throwable"});
+      mv.visitVarInsn(ASTORE, 2);
+      mv.visitVarInsn(ILOAD, 1); // push x on the stack again (should be same as above).
+      // mv.visitInsn(ICONST_0);
+      mv.visitLabel(l5);
+      mv.visitLineNumber(16, l5);
+      // mv.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
+      mv.visitFrame(
+          Opcodes.F_FULL,
+          1,
+          new Object[] {"com/android/tools/r8/debug/LocalEndTest"},
+          1,
+          new Object[] {Opcodes.INTEGER});
+      // Load the on-stack copy of x
+      mv.visitVarInsn(ISTORE, 1);
+      Label l6 = new Label();
+      mv.visitLabel(l6);
+      mv.visitLineNumber(17, l6);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
+      Label l7 = new Label();
+      mv.visitLabel(l7);
+      mv.visitLineNumber(18, l7);
+      mv.visitInsn(RETURN);
+      Label l8 = new Label();
+      mv.visitLabel(l8);
+      mv.visitLocalVariable("x", "I", null, l0, l5, 1);
+      mv.visitLocalVariable("this", "Lcom/android/tools/r8/debug/LocalEndTest;", null, l3, l8, 0);
+      mv.visitLocalVariable("y", "I", null, l6, l8, 1);
+      mv.visitMaxs(2, 3);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PRIVATE, "bar", "()V", null, null);
+      mv.visitCode();
+      Label l0 = new Label();
+      mv.visitLabel(l0);
+      mv.visitLineNumber(22, l0);
+      mv.visitInsn(RETURN);
+      Label l1 = new Label();
+      mv.visitLabel(l1);
+      mv.visitLocalVariable("this", "Lcom/android/tools/r8/debug/LocalEndTest;", null, l0, l1, 0);
+      mv.visitMaxs(0, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+      mv.visitCode();
+      Label l0 = new Label();
+      mv.visitLabel(l0);
+      mv.visitLineNumber(25, l0);
+      mv.visitTypeInsn(NEW, "com/android/tools/r8/debug/LocalEndTest");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "com/android/tools/r8/debug/LocalEndTest", "<init>", "()V", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "com/android/tools/r8/debug/LocalEndTest", "foo", "()V", false);
+      Label l1 = new Label();
+      mv.visitLabel(l1);
+      mv.visitLineNumber(26, l1);
+      mv.visitInsn(RETURN);
+      Label l2 = new Label();
+      mv.visitLabel(l2);
+      mv.visitLocalVariable("args", "[Ljava/lang/String;", null, l0, l2, 0);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    cw.visitEnd();
+
+    return cw.toByteArray();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java b/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java
new file mode 100644
index 0000000..982332e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java
@@ -0,0 +1,71 @@
+// 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 com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class LocalEndTestRunner extends DebugTestBase {
+
+  static final Class CLASS = LocalEndTest.class;
+  static final String NAME = CLASS.getCanonicalName();
+  static final String DESC = DescriptorUtils.javaTypeToDescriptor(NAME);
+  static final String FILE = CLASS.getSimpleName() + ".java";
+
+  private static Path inputJarCache = null;
+
+  private final String name;
+  private final DebugTestConfig config;
+
+  @Parameters(name = "{0}")
+  public static Collection<Object[]> setup() {
+    DelayedDebugTestConfig cf = temp -> new CfDebugTestConfig().addPaths(getInputJar(temp));
+    DelayedDebugTestConfig d8 =
+        temp -> new D8DebugTestConfig().compileAndAdd(temp, getInputJar(temp));
+    return ImmutableList.of(new Object[] {"CF", cf}, new Object[] {"D8", d8});
+  }
+
+  private static Path getInputJar(TemporaryFolder temp) {
+    if (inputJarCache == null) {
+      inputJarCache = temp.getRoot().toPath().resolve("input.jar");
+      ClassFileConsumer jarWriter = new ArchiveConsumer(inputJarCache);
+      jarWriter.accept(LocalEndTestDump.dump(), DESC, null);
+      jarWriter.finished(null);
+    }
+    return inputJarCache;
+  }
+
+  public LocalEndTestRunner(String name, DelayedDebugTestConfig config) {
+    this.name = name;
+    this.config = config.getConfig(temp);
+  }
+
+  @Test
+  public void test() throws Throwable {
+    runDebugTest(
+        config,
+        NAME,
+        breakpoint(NAME, "foo", 13),
+        run(),
+        checkLine(FILE, 13),
+        checkLocal("x"),
+        stepOver(),
+        checkLine(FILE, 14),
+        checkLocal("x"),
+        stepOver(),
+        checkLine(FILE, 16),
+        checkNoLocal("x"),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/PostIncrementTest.java b/src/test/java/com/android/tools/r8/debug/PostIncrementTest.java
new file mode 100644
index 0000000..543a5a1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/PostIncrementTest.java
@@ -0,0 +1,23 @@
+// 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 PostIncrementTest {
+
+  static final int LENGTH = 4 * 16;
+
+  private static void loop(int[] a) {
+    int i = 0;
+    int s = 128;
+    while (i++ < LENGTH - 2) {
+      if (i % 2 == 0) {
+        a[i] = s++;
+      }
+    }
+  }
+
+  public static void main(String[] args) {
+    loop(new int[LENGTH]);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java b/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java
new file mode 100644
index 0000000..c805c15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debug/PostIncrementTestRunner.java
@@ -0,0 +1,30 @@
+// 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 com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.DebuggeeState;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+// See b/80385846
+public class PostIncrementTestRunner extends DebugTestBase {
+
+  private static final Class CLASS = PostIncrementTest.class;
+  private static final String NAME = CLASS.getCanonicalName();
+
+  @Test
+  public void test() throws Exception {
+    DebugTestConfig cfConfig = new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
+    DebugTestConfig d8Config = new D8DebugTestConfig().compileAndAddClasses(temp, CLASS);
+    new DebugStreamComparator()
+        .add("CF", createStream(cfConfig))
+        .add("D8", createStream(d8Config))
+        .compare();
+  }
+
+  private Stream<DebuggeeState> createStream(DebugTestConfig config) throws Exception {
+    return streamDebugTest(config, NAME, ANDROID_FILTER);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
index a23f3f4..17f0c04 100644
--- a/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
+++ b/src/test/java/com/android/tools/r8/debug/SynchronizedBlockTest.java
@@ -5,25 +5,61 @@
 
 import static org.hamcrest.core.IsNot.not;
 
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Collection;
 import org.junit.Assume;
-import org.junit.BeforeClass;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
-/**
- * Test single stepping behaviour of synchronized blocks.
- */
+/** Test single stepping behaviour of synchronized blocks. */
+@RunWith(Parameterized.class)
 public class SynchronizedBlockTest extends DebugTestBase {
 
   public static final String CLASS = "SynchronizedBlock";
   public static final String FILE = "SynchronizedBlock.java";
 
-  private static DebugTestConfig config;
+  private final String name;
+  private final DebugTestConfig config;
 
-  @BeforeClass
-  public static void setup() {
-    config = new D8DebugTestResourcesConfig(temp);
+  @Parameterized.Parameters(name = "{0}")
+  public static Collection<Object[]> setup() {
+    DelayedDebugTestConfig cf =
+        temp -> new CfDebugTestConfig().addPaths(DebugTestBase.DEBUGGEE_JAR);
+    DelayedDebugTestConfig r8cf = temp -> new R8CfDebugTestResourcesConfig(temp);
+    DelayedDebugTestConfig r8cfcf =
+        temp -> {
+          Path out = temp.getRoot().toPath().resolve("r8cfcf.jar");
+          try {
+            ToolHelper.runR8(
+                R8Command.builder()
+                    .setOutput(out, OutputMode.ClassFile)
+                    .setMode(CompilationMode.DEBUG)
+                    .addProgramFiles(DebugTestBase.DEBUGGEE_JAR)
+                    .build(),
+                options -> options.enableCfFrontend = true);
+          } catch (Exception e) {
+            throw new RuntimeException(e);
+          }
+          return new CfDebugTestConfig().addPaths(out);
+        };
+    DelayedDebugTestConfig d8 = temp -> new D8DebugTestResourcesConfig(temp);
+    return ImmutableList.of(
+        new Object[] {"CF", cf},
+        new Object[] {"D8", d8},
+        new Object[] {"R8/CF", r8cf},
+        new Object[] {"R8/CF/CF", r8cfcf});
+  }
+
+  public SynchronizedBlockTest(String name, DelayedDebugTestConfig config) {
+    this.name = name;
+    this.config = config.getConfig(temp);
   }
 
   @Test
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 d56c0e7..a148a26 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -753,6 +753,12 @@
     }
 
     @Override
+    public void buildBlockTransfer(
+        IRBuilder builder, int predecessorOffset, int successorOffset, boolean isExceptional) {
+      throw new Unreachable();
+    }
+
+    @Override
     public void buildPostlude(IRBuilder builder) {
       // Intentionally empty.
     }
diff --git a/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeInSubInterfaceTest.java b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeInSubInterfaceTest.java
new file mode 100644
index 0000000..6602d0c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/CovariantReturnTypeInSubInterfaceTest.java
@@ -0,0 +1,106 @@
+// 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.naming;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Ignore;
+import org.junit.Test;
+
+interface SuperInterface {
+  Super foo();
+}
+
+interface SubInterface extends SuperInterface {
+  @Override
+  Sub foo();
+}
+
+class Super {
+  protected int bar() {
+    return 0;
+  }
+}
+
+class Sub extends Super {
+  @Override
+  protected int bar() {
+    return 1;
+  }
+}
+
+class SuperImplementer implements SuperInterface {
+  @Override
+  public Super foo() {
+    return new Super();
+  }
+}
+
+class SubImplementer implements SubInterface {
+  @Override
+  public Sub foo() {
+    return new Sub();
+  }
+}
+
+class TestMain {
+  public static void main(String[] args) {
+    SubImplementer subImplementer = new SubImplementer();
+    Super sup = subImplementer.foo();
+    System.out.println(sup.bar());
+  }
+}
+
+public class CovariantReturnTypeInSubInterfaceTest extends TestBase {
+
+  @Ignore("b/112185748")
+  @Test
+  public void test() throws Exception {
+    List<String> config = ImmutableList.of(
+        "-printmapping",
+        "-useuniqueclassmembernames",
+        "-keep class " + TestMain.class.getCanonicalName() + " {",
+        "  public void main(...);",
+        "}",
+        "-keep,allowobfuscation class **.Super* {",
+        "  <methods>;",
+        "}",
+        "-keep,allowobfuscation class **.Sub* {",
+        "  <methods>;",
+        "}"
+    );
+    AndroidApp app = readClasses(
+        SuperInterface.class,
+        SubInterface.class,
+        Super.class,
+        Sub.class,
+        SuperImplementer.class,
+        SubImplementer.class,
+        TestMain.class
+    );
+    AndroidApp processedApp = compileWithR8(app, String.join(System.lineSeparator(), config));
+    CodeInspector inspector = new CodeInspector(processedApp);
+    ClassSubject superInterface = inspector.clazz(SuperInterface.class);
+    assertThat(superInterface, isRenamed());
+    MethodSubject foo1 = superInterface.method(
+        Super.class.getCanonicalName(), "foo", ImmutableList.of());
+    assertThat(foo1, isRenamed());
+    ClassSubject subInterface = inspector.clazz(SubInterface.class);
+    assertThat(subInterface, isRenamed());
+    MethodSubject foo2 = subInterface.method(
+        Sub.class.getCanonicalName(), "foo", ImmutableList.of());
+    assertThat(foo2, isRenamed());
+    assertEquals(foo1.getFinalName(), foo2.getFinalName());
+  }
+
+}