Insert local changes between blocks in split edges.

This CL refactors the IRBuilder and SourceCode companions to have a buildBlockTransfer callback which is responsible for emitting the local variable changes between blocks.

In addition, the use of debug-value reads is now only needed explicitly where a local variable ends, which can be:
1. between two instructions within a basic block (either it just ends, or the local is written to)
2. between two basic blocks, in which case it now always ends prior to entry of the successor, this happens in one of three ways:
 a. if only one successor exists, by updating the locals prior to exit of the block; or
 b. if multiple successors exist, for each successor:
   - if the successor has only this as predecessor, emit the local change on entry, but with the position of the predecessor;
   - otherwise, insert a split block and closing the local in the split block;

A local end ensures that the local remains live until the end and is then used to explicitly end the liveness at that point (even if the SSA value may continue being live after that point).
For this reason, whenever a local should start, we now must emit a DebugLocalWrite introducing it even if the source of the write is an SSA value associated with the same local, since it may have been previously ended.

Bug: 80385846
Bug: 78611610

Change-Id: I94e4ac1f00a0b849f29d727a2509376858519808
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.
     }