Associate a non-none position to all instructions in debug mode.

Bug: 78619571, 110873173
Change-Id: I1a3d4a8b7da2408cc67664a2c3a6ecc884245bb6
diff --git a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
index 12c1321..aa3457b 100644
--- a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
@@ -63,8 +63,9 @@
             }
           }
           InstructionListIterator it = pred.listIterator(pred.getInstructions().size());
-          it.previous();
-          movePhis(moves, it);
+          Instruction exit = it.previous();
+          assert pred.exit() == exit;
+          movePhis(moves, it, exit.getPosition());
         }
         allocator.addToLiveAtEntrySet(block, block.getPhis());
       }
@@ -137,13 +138,13 @@
     }
   }
 
-  private void movePhis(List<PhiMove> moves, InstructionListIterator it) {
+  private void movePhis(List<PhiMove> moves, InstructionListIterator it, Position position) {
     // TODO(zerny): Accounting for non-interfering phis would lower the max stack size.
     int topOfStack = 0;
     List<StackValue> temps = new ArrayList<>(moves.size());
     for (PhiMove move : moves) {
       StackValue tmp = createStackValue(move.phi, topOfStack++);
-      add(load(tmp, move.operand), move.phi.getBlock(), Position.none(), it);
+      add(load(tmp, move.operand), move.phi.getBlock(), position, it);
       temps.add(tmp);
       move.operand.removePhiUser(move.phi);
     }
@@ -151,7 +152,7 @@
       PhiMove move = moves.get(i);
       StackValue tmp = temps.get(i);
       FixedLocalValue out = new FixedLocalValue(move.phi);
-      add(new Store(out, tmp), move.phi.getBlock(), Position.none(), it);
+      add(new Store(out, tmp), move.phi.getBlock(), position, it);
       move.phi.replaceUsers(out);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
index bfc0a9f..18f23e3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEventBuilder.java
@@ -82,16 +82,8 @@
     Position position = instruction.getPosition();
     boolean pcAdvancing = pc != postPc;
 
-    // In release mode we can only check that all throwing instructions have positions.
-    // See IRCode's isConsistentGraph and computeAllThrowingInstructionsHavePositions.
-
     // In debug mode check that all non-nop instructions have positions.
-    assert startLine == NO_LINE_INFO || !hasDebugPositions || !pcAdvancing || position.isSome()
-        : "PC-advancing instruction " + instruction + " expected to have an associated position.";
-
-    // In any mode check that nop instructions have no position info.
-    assert pcAdvancing || position.isNone()
-        : "Nop instruction " + instruction + " must never have an associated position.";
+    assert instruction.verifyValidPositionInfo(options.debug);
 
     if (instruction.isArgument()) {
       startArgument(instruction.asArgument());
diff --git a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
index cd6a731..91be1a4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
+++ b/src/main/java/com/android/tools/r8/ir/code/AlwaysMaterializingUser.java
@@ -16,8 +16,6 @@
 
   public AlwaysMaterializingUser(Value src) {
     super(null, src);
-    // The user instruction never materializes so ensure it has position none.
-    setPosition(Position.none());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Argument.java b/src/main/java/com/android/tools/r8/ir/code/Argument.java
index ac1ed7a..b5fea0b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Argument.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Argument.java
@@ -24,12 +24,6 @@
   public Argument(Value outValue) {
     super(outValue);
     outValue.markAsArgument();
-    super.setPosition(Position.none());
-  }
-
-  @Override
-  public void setPosition(Position position) {
-    // Arguments never have positional information as they never materialize to actual instructions.
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index a2962ab..51ed473 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -47,9 +47,9 @@
 
   private Int2ReferenceMap<DebugLocalInfo> localsAtEntry;
 
-  public boolean consistentBlockInstructions(boolean argumentsAllowed) {
+  public boolean consistentBlockInstructions(boolean argumentsAllowed, boolean debug) {
     for (Instruction instruction : getInstructions()) {
-      assert instruction.getPosition() != null;
+      assert instruction.verifyValidPositionInfo(debug);
       assert instruction.getBlock() == this;
       assert !instruction.isArgument() || argumentsAllowed;
       assert !instruction.isDebugLocalRead() || !instruction.getDebugValues().isEmpty();
@@ -290,6 +290,7 @@
           }
           Instruction exit = new Goto();
           exit.setBlock(this);
+          exit.setPosition(instruction.getPosition());
           getInstructions().addLast(exit);
         } else if (indexOfOldBlock >= successors.size() - 2) {
           // Old is either true or fallthrough and we need to swap the new block into the right
@@ -1099,14 +1100,14 @@
   /**
    * Create a new basic block with a single goto instruction.
    *
-   * <p>The constructed basic block has no predecessors and has one
-   * successors which is the target block.
+   * <p>The constructed basic block has no predecessors and has one successor which is the target
+   * block.
    *
    * @param blockNumber the block number of the goto block
    * @param target the target of the goto block
    */
-  public static BasicBlock createGotoBlock(int blockNumber, BasicBlock target) {
-    BasicBlock block = createGotoBlock(blockNumber);
+  public static BasicBlock createGotoBlock(int blockNumber, Position position, BasicBlock target) {
+    BasicBlock block = createGotoBlock(blockNumber, position);
     block.getSuccessors().add(target);
     return block;
   }
@@ -1118,11 +1119,12 @@
    *
    * @param blockNumber the block number of the goto block
    */
-  public static BasicBlock createGotoBlock(int blockNumber) {
+  public static BasicBlock createGotoBlock(int blockNumber, Position position) {
     BasicBlock block = new BasicBlock();
     block.add(new Goto());
     block.close(null);
     block.setNumber(blockNumber);
+    block.entry().setPosition(position);
     return block;
   }
 
@@ -1228,8 +1230,7 @@
   }
 
   public Position getPosition() {
-    BasicBlock block = endOfGotoChain();
-    return block != null ? block.entry().getPosition() : Position.none();
+    return entry().getPosition();
   }
 
   public boolean hasOneNormalExit() {
@@ -1459,7 +1460,9 @@
         newBlock.add(newMove);
         newMove.setPosition(position);
       }
-      newBlock.add(new Goto());
+      Goto next = new Goto();
+      next.setPosition(position);
+      newBlock.add(next);
       newBlock.close(null);
       newBlock.getSuccessors().add(this);
       newBlock.getPredecessors().add(predecessor);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
index 2fcbed1..08d7a07 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
@@ -193,6 +193,9 @@
     // Don't allow splitting after the last instruction.
     assert hasNext();
 
+    // Get the position at which the block is being split.
+    Position position = current != null ? current.getPosition() : block.getPosition();
+
     // Prepare the new block, placing the exception handlers on the block with the throwing
     // instruction.
     boolean keepCatchHandlers = hasPrevious() && peekPrevious().instructionTypeCanThrow();
@@ -201,6 +204,7 @@
     // Add a goto instruction.
     Goto newGoto = new Goto(block);
     listIterator.add(newGoto);
+    newGoto.setPosition(position);
 
     // Move all remaining instructions to the new block.
     while (listIterator.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 6f5fa47..73cf1c2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -118,7 +118,6 @@
   @Override
   public void buildDex(DexBuilder builder) {
     if (!dest().needsRegister()) {
-      forceSetPosition(Position.none());
       builder.addNothing(this);
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
index 5de4b91..c956593 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
@@ -35,6 +35,11 @@
     throw new Unreachable();
   }
 
+  @Override
+  public boolean verifyValidPositionInfo(boolean debug) {
+    return true;
+  }
+
   public Int2ReferenceMap<DebugLocalInfo> getEnding() {
     return ending;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 80a76fe..972d2d2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -15,7 +15,6 @@
 
   public Goto() {
     super(null);
-    super.setPosition(Position.none());
   }
 
   public Goto(BasicBlock block) {
@@ -23,12 +22,6 @@
     setBlock(block);
   }
 
-  @Override
-  public void setPosition(Position position) {
-    // In general goto's do not signify program points only transitions, so we avoid
-    // associating them with positional information.
-  }
-
   public BasicBlock getTarget() {
     assert getBlock().exit() == this;
     List<BasicBlock> successors = getBlock().getSuccessors();
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 96cc33a..1cafe2a 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
@@ -95,6 +95,7 @@
       ValueNumberGenerator valueNumberGenerator,
       boolean hasDebugPositions,
       boolean hasConstString) {
+    assert options != null;
     this.options = options;
     this.method = method;
     this.blocks = blocks;
@@ -212,7 +213,8 @@
           // correct predecessor and successor structure. It is inserted
           // at the end of the list of blocks disregarding branching
           // structure.
-          BasicBlock newBlock = BasicBlock.createGotoBlock(nextBlockNumber++, block);
+          BasicBlock newBlock =
+              BasicBlock.createGotoBlock(nextBlockNumber++, pred.exit().getPosition(), block);
           newBlocks.add(newBlock);
           pred.replaceSuccessor(block, newBlock);
           newBlock.getPredecessors().add(pred);
@@ -268,7 +270,9 @@
           fallthrough = fallthrough.exit().fallthroughBlock();
         }
         if (fallthrough != null) {
-          BasicBlock newFallthrough = BasicBlock.createGotoBlock(nextBlockNumber++, fallthrough);
+          BasicBlock newFallthrough =
+              BasicBlock.createGotoBlock(
+                  nextBlockNumber++, block.exit().getPosition(), fallthrough);
           current.exit().setFallthroughBlock(newFallthrough);
           newFallthrough.getPredecessors().add(current);
           fallthrough.replacePredecessor(current, newFallthrough);
@@ -563,7 +567,7 @@
   private boolean consistentBlockInstructions() {
     boolean argumentsAllowed = true;
     for (BasicBlock block : blocks) {
-      block.consistentBlockInstructions(argumentsAllowed);
+      block.consistentBlockInstructions(argumentsAllowed, options.debug);
       argumentsAllowed = false;
     }
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index bae912d..6f62d21 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -1116,4 +1116,11 @@
   public boolean triggersInitializationOfClass(DexType klass) {
     return false;
   }
+
+  public boolean verifyValidPositionInfo(boolean debug) {
+    assert position != null;
+    assert !debug || getPosition().isSome();
+    assert !instructionTypeCanThrow() || getPosition().isSome() || getPosition().isSyntheticNone();
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Position.java b/src/main/java/com/android/tools/r8/ir/code/Position.java
index 74aaadf..ec923ee 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Position.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Position.java
@@ -5,12 +5,22 @@
 
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
+import com.google.common.annotations.VisibleForTesting;
 import java.util.Objects;
 
 public class Position {
 
+  // A no-position marker. Not having a position means the position is implicitly defined by the
+  // context, e.g., the marker does not materialize anything concrete.
   private static final Position NO_POSITION = new Position(-1, null, null, null, false);
 
+  // A synthetic marker position that should never materialize.
+  // This is used specifically to mark exceptional exit blocks from synchronized methods in release.
+  private static final Position NO_POSITION_SYNTHETIC = new Position(-1, null, null, null, true);
+
+  // Fake position to use for representing an actual position in testing code.
+  private static final Position TESTING_POSITION = new Position(0, null, null, null, true);
+
   public final int line;
   public final DexString file;
   public final boolean synthetic;
@@ -26,6 +36,7 @@
   public Position(int line, DexString file, DexMethod method, Position callerPosition) {
     this(line, file, method, callerPosition, false);
     assert line >= 0;
+    assert method != null;
   }
 
   private Position(
@@ -36,11 +47,11 @@
     this.method = method;
     this.callerPosition = callerPosition;
     assert callerPosition == null || callerPosition.method != null;
-    assert line == -1 || method != null; // It's NO_POSITION or must have valid method.
   }
 
   public static Position synthetic(int line, DexMethod method, Position callerPosition) {
     assert line >= 0;
+    assert method != null;
     return new Position(line, null, method, callerPosition, true);
   }
 
@@ -48,6 +59,15 @@
     return NO_POSITION;
   }
 
+  public static Position syntheticNone() {
+    return NO_POSITION_SYNTHETIC;
+  }
+
+  @VisibleForTesting
+  public static Position testingPosition() {
+    return TESTING_POSITION;
+  }
+
   // This factory method is used by the Inliner to create Positions when the caller has no valid
   // positions. Since the callee still may have valid positions we need a non-null Position to set
   // it as the caller of the inlined Positions.
@@ -60,6 +80,10 @@
     return line == -1;
   }
 
+  public boolean isSyntheticNone() {
+    return this == NO_POSITION_SYNTHETIC;
+  }
+
   public boolean isSome() {
     return !isNone();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 4d0a5ba..37a0150 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -139,9 +139,7 @@
     registerAllocator.allocateRegisters();
     loadStoreHelper.insertPhiMoves(registerAllocator);
     CodeRewriter.collapsTrivialGotos(method, code);
-    int instructionTableCount =
-        DexBuilder.instructionNumberToIndex(code.numberRemainingInstructions());
-    DexBuilder.removeRedundantDebugPositions(code, instructionTableCount);
+    DexBuilder.removeRedundantDebugPositions(code);
     CfCode code = buildCfCode();
     return code;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 07107a5..0084a74 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -50,7 +50,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.InstructionIterator;
-import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Move;
 import com.android.tools.r8.ir.code.NewArrayFilledData;
 import com.android.tools.r8.ir.code.Position;
@@ -65,7 +64,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
 import java.util.List;
@@ -158,7 +156,7 @@
 
       // Remove redundant debug position instructions. They would otherwise materialize as
       // unnecessary nops.
-      removeRedundantDebugPositions(ir, instructionToInfo.length);
+      removeRedundantDebugPositions(ir);
 
       // Populate the builder info objects.
       numberOfInstructions = 0;
@@ -251,114 +249,95 @@
     return code;
   }
 
-  private static boolean verifyNopHasNoPosition(
-      com.android.tools.r8.ir.code.Instruction instruction, ListIterator<BasicBlock> blocks) {
-    BasicBlock nextBlock = null;
-    if (blocks.hasNext()) {
-      nextBlock = blocks.next();
-      blocks.previous();
-    }
-    return verifyNopHasNoPosition(instruction, nextBlock);
-  }
-
-  private static boolean verifyNopHasNoPosition(
-      com.android.tools.r8.ir.code.Instruction instruction, BasicBlock nextBlock) {
-    if (isNopInstruction(instruction, nextBlock)) {
-      assert instruction.getPosition().isNone();
-    }
-    return true;
-  }
-
   // Eliminates unneeded debug positions.
   //
-  // After this pass all instructions that don't materialize to an actual DEX/CF instruction will
-  // have Position.none(). If any other instruction has a non-none position then all other
-  // instructions that do materialize to a DEX/CF instruction (eg, non-fallthrough gotos) will have
-  // a non-none position.
-  //
-  // Remaining debug positions indicate two successive lines without intermediate instructions.
-  // For these we must emit a nop instruction to ensure they don't share the same pc.
-  public static void removeRedundantDebugPositions(IRCode code, int instructionTableSize) {
+  // After this pass all remaining debug positions mark places where we must ensure a materializing
+  // instruction, eg, for two successive lines without intermediate instructions.
+  public static void removeRedundantDebugPositions(IRCode code) {
     if (!code.hasDebugPositions) {
       return;
     }
-    Int2ReferenceMap[] localsMap = new Int2ReferenceMap[instructionTableSize];
-    // Scan forwards removing debug positions equal to the previous instruction position.
-    {
-      Int2ReferenceMap<DebugLocalInfo> locals = Int2ReferenceMaps.emptyMap();
-      Position previous = Position.none();
-      ListIterator<BasicBlock> blockIterator = code.listIterator();
-      BasicBlock previousBlock = null;
-      while (blockIterator.hasNext()) {
-        BasicBlock block = blockIterator.next();
-        if (previousBlock != null
-            && previousBlock.exit().isGoto()
-            && !isNopInstruction(previousBlock.exit(), block)) {
-          assert previousBlock.exit().getPosition().isNone()
-              || previousBlock.exit().getPosition().equals(previous);
-          previousBlock.exit().forceSetPosition(previous);
+    // Current position known to have a materializing instruction associated with it.
+    Position currentMaterializedPosition = Position.none();
+
+    // Current debug-position marker that is not yet known to have another instruction materializing
+    // to the same position.
+    DebugPosition unresolvedPosition = null;
+
+    // Locals live at the debug-position marker. These must also be the same at a possible
+    // materializing instruction with the same position for it to be sound to remove the marker.
+    Int2ReferenceMap<DebugLocalInfo> localsAtUnresolvedPosition = null;
+
+    // Compute the set of all positions that can be removed.
+    // (Delaying removal to avoid ConcurrentModificationException).
+    List<DebugPosition> toRemove = new ArrayList<>();
+
+    for (int blockIndex = 0; blockIndex < code.blocks.size(); blockIndex++) {
+      BasicBlock currentBlock = code.blocks.get(blockIndex);
+      BasicBlock nextBlock =
+          blockIndex + 1 < code.blocks.size() ? code.blocks.get(blockIndex + 1) : null;
+
+      // Current materialized position can remain on block entry only if it is also the exit of
+      // the blocks predecessors. If not, we cannot ensure the jumps will hit this line unless
+      // another instruction materialized the position.
+      for (BasicBlock pred : currentBlock.getPredecessors()) {
+        if (pred.exit().getPosition() != currentMaterializedPosition) {
+          currentMaterializedPosition = Position.none();
+          break;
         }
-        InstructionListIterator instructionIterator = block.listIterator();
-        if (block.getLocalsAtEntry() != null && !locals.equals(block.getLocalsAtEntry())) {
-          locals = new Int2ReferenceOpenHashMap<>(block.getLocalsAtEntry());
-        }
-        while (instructionIterator.hasNext()) {
-          com.android.tools.r8.ir.code.Instruction instruction = instructionIterator.next();
-          if (instruction.isDebugPosition() && previous.equals(instruction.getPosition())) {
-            instructionIterator.remove();
-          } else if (isNonMaterializingConstNumber(instruction)) {
-            instruction.forceSetPosition(Position.none());
-          } else if (instruction.getPosition().isSome()) {
-            assert verifyNopHasNoPosition(instruction, blockIterator);
-            previous = instruction.getPosition();
+      }
+
+      // Current locals.
+      Int2ReferenceMap<DebugLocalInfo> locals =
+          currentBlock.getLocalsAtEntry() != null
+              ? new Int2ReferenceOpenHashMap<>(currentBlock.getLocalsAtEntry())
+              : new Int2ReferenceOpenHashMap<>();
+
+      InstructionIterator iterator = currentBlock.iterator();
+      while (iterator.hasNext()) {
+        com.android.tools.r8.ir.code.Instruction instruction = iterator.next();
+        if (instruction.isDebugPosition()) {
+          if (unresolvedPosition == null
+              && currentMaterializedPosition == instruction.getPosition()) {
+            // Here we don't need to check locals state as the line is already active.
+            toRemove.add(instruction.asDebugPosition());
+          } else if (unresolvedPosition != null
+              && unresolvedPosition.getPosition() == instruction.getPosition()
+              && locals.equals(localsAtUnresolvedPosition)) {
+            toRemove.add(instruction.asDebugPosition());
+          } else {
+            unresolvedPosition = instruction.asDebugPosition();
+            localsAtUnresolvedPosition = new Int2ReferenceOpenHashMap<>(locals);
           }
+        } else if (instruction.getPosition().isSome()) {
+          if (!isNopInstruction(instruction, nextBlock)) {
+            if (unresolvedPosition != null) {
+              if (unresolvedPosition.getPosition() == instruction.getPosition()
+                  && locals.equals(localsAtUnresolvedPosition)) {
+                toRemove.add(unresolvedPosition);
+              }
+              unresolvedPosition = null;
+              localsAtUnresolvedPosition = null;
+            }
+            currentMaterializedPosition = instruction.getPosition();
+          }
+        } else {
+          // Only local-change instructions don't have positions in debug mode but fail gracefully.
+          assert instruction.isDebugLocalsChange();
           if (instruction.isDebugLocalsChange()) {
-            locals = new Int2ReferenceOpenHashMap<>(locals);
             instruction.asDebugLocalsChange().apply(locals);
           }
-          localsMap[instructionNumberToIndex(instruction.getNumber())] = locals;
         }
-        previousBlock = block;
-      }
-      if (previousBlock != null && previousBlock.exit().isGoto()) {
-        // If the last block ends in a goto it cannot be a fallthrough/nop.
-        assert previousBlock.exit().getPosition().isNone();
-        previousBlock.exit().forceSetPosition(previous);
       }
     }
-    // Scan backwards removing debug positions equal to the following instruction position.
-    {
-      ListIterator<BasicBlock> blocks = code.blocks.listIterator(code.blocks.size());
-      BasicBlock block = null;
-      BasicBlock nextBlock;
-      com.android.tools.r8.ir.code.Instruction next = null;
-      Int2ReferenceMap nextLocals = null;
-      while (blocks.hasPrevious()) {
-        nextBlock = block;
-        block = blocks.previous();
-        InstructionListIterator instructions = block.listIterator(block.getInstructions().size());
-        while (instructions.hasPrevious()) {
-          com.android.tools.r8.ir.code.Instruction instruction = instructions.previous();
-          int index = instructionNumberToIndex(instruction.getNumber());
-          if (instruction.isDebugPosition() && localsMap[index].equals(nextLocals)) {
-            Position nextPosition = next.getPosition();
-            Position thisPosition = instruction.getPosition();
-            if (nextPosition.isNone()) {
-              next.forceSetPosition(thisPosition);
-              instructions.remove();
-            } else if (nextPosition.equals(thisPosition)) {
-              instructions.remove();
-            } else {
-              next = instruction;
-            }
-          } else {
-            assert verifyNopHasNoPosition(instruction, nextBlock);
-            if (!isNopInstruction(instruction, nextBlock)) {
-              next = instruction;
-              nextLocals = localsMap[index];
-              assert nextLocals != null;
-            }
-          }
+    // Remove all unneeded positions.
+    if (!toRemove.isEmpty()) {
+      InstructionIterator it = code.instructionIterator();
+      int i = 0;
+      while (it.hasNext() && i < toRemove.size()) {
+        if (it.next() == toRemove.get(i)) {
+          it.removeOrReplaceByDebugLocalRead();
+          ++i;
         }
       }
     }
@@ -385,7 +364,8 @@
       if (ifsNeedingRewrite.contains(block)) {
         If theIf = block.exit().asIf();
         BasicBlock trueTarget = theIf.getTrueTarget();
-        BasicBlock newBlock = BasicBlock.createGotoBlock(ir.blocks.size(), trueTarget);
+        BasicBlock newBlock =
+            BasicBlock.createGotoBlock(ir.blocks.size(), theIf.getPosition(), trueTarget);
         theIf.setTrueTarget(newBlock);
         theIf.invert();
         it.add(newBlock);
@@ -438,7 +418,6 @@
   }
 
   public void addNothing(com.android.tools.r8.ir.code.Instruction instruction) {
-    assert instruction.getPosition().isNone();
     add(instruction, new FallThroughInfo(instruction));
   }
 
@@ -496,7 +475,6 @@
   public void addReturn(Return ret, Instruction dex) {
     if (nextBlock != null
         && ret.identicalAfterRegisterAllocation(nextBlock.entry(), registerAllocator)) {
-      ret.forceSetPosition(Position.none());
       addNothing(ret);
     } else {
       add(ret, dex);
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 d14273e..d855e0e 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
@@ -153,10 +153,16 @@
   private static class SplitBlockWorklistItem extends WorklistItem {
     private final int sourceOffset;
     private final int targetOffset;
+    private final Position position;
 
     public SplitBlockWorklistItem(
-        int firstInstructionIndex, BasicBlock block, int sourceOffset, int targetOffset) {
+        int firstInstructionIndex,
+        BasicBlock block,
+        Position position,
+        int sourceOffset,
+        int targetOffset) {
       super(block, firstInstructionIndex);
+      this.position = position;
       this.sourceOffset = sourceOffset;
       this.targetOffset = targetOffset;
     }
@@ -473,6 +479,7 @@
 
     // Insert definitions for all uninitialized local values.
     if (uninitializedDebugLocalValues != null) {
+      Position position = entryBlock.getPosition();
       InstructionListIterator it = entryBlock.listIterator();
       it.nextUntil(i -> !i.isArgument());
       it.previous();
@@ -481,7 +488,7 @@
           if (value.isUsed()) {
             Instruction def = new DebugLocalUninitialized(value);
             def.setBlock(entryBlock);
-            def.setPosition(Position.none());
+            def.setPosition(position);
             it.add(def);
           }
         }
@@ -515,11 +522,11 @@
       resolver.resolve(ir, this);
     }
 
+    assert ir.isConsistentSSA();
+
     // Clear the code so we don't build multiple times.
     source.clear();
     source = null;
-
-    assert ir.isConsistentSSA();
     return ir;
   }
 
@@ -631,7 +638,7 @@
             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());
+          addInstruction(new Goto(), splitEdgeItem.position);
           closeCurrentBlockGuaranteedNotToNeedEdgeSplitting();
           continue;
         } else if (!debugLocalEnds.isEmpty()) {
@@ -768,10 +775,15 @@
 
   public void addDebugPosition(Position position) {
     if (options.debug) {
+      assert previousLocalValue == null;
       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());
+        if (currentBlock.getInstructions().isEmpty()) {
+          addInstruction(new DebugLocalRead());
+        } else {
+          attachLocalValues(currentBlock.getInstructions().getLast());
+        }
       }
       addInstruction(new DebugPosition());
     }
@@ -2126,6 +2138,7 @@
     assert currentBlock != null;
     assert currentBlock.isEmpty() || !currentBlock.getInstructions().getLast().isJumpInstruction();
     BlockInfo info = getBlockInfo(currentBlock);
+    Position position = source.getCurrentPosition();
     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.
@@ -2145,13 +2158,14 @@
               new SplitBlockWorklistItem(
                   oldItem.firstInstructionIndex,
                   oldItem.block,
+                  position,
                   currentInstructionOffset,
                   successorOffset));
         } else {
           BasicBlock splitBlock = createSplitEdgeBlock(currentBlock, successorInfo.block);
           ssaWorklist.add(
               new SplitBlockWorklistItem(
-                  -1, splitBlock, currentInstructionOffset, successorOffset));
+                  -1, splitBlock, position, currentInstructionOffset, successorOffset));
         }
       }
     } else if (info.normalSuccessors.size() == 1) {
@@ -2230,7 +2244,9 @@
             int otherPredecessorIndex = values.get(v);
             BasicBlock joinBlock = joinBlocks.get(otherPredecessorIndex);
             if (joinBlock == null) {
-              joinBlock = BasicBlock.createGotoBlock(blocks.size() + blocksToAdd.size(), block);
+              joinBlock =
+                  BasicBlock.createGotoBlock(
+                      blocks.size() + blocksToAdd.size(), block.getPosition(), block);
               joinBlocks.put(otherPredecessorIndex, joinBlock);
               blocksToAdd.add(joinBlock);
               BasicBlock otherPredecessor = block.getPredecessors().get(otherPredecessorIndex);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index f4a1e4e..6890595 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -1076,6 +1076,7 @@
     // Forced user of the forced definition to ensure it has a user and thus live range.
     Instruction fixitUser = new AlwaysMaterializingUser(fixitValue);
     fixitUser.setBlock(addBefore.getBlock());
+    fixitUser.setPosition(addBefore.getPosition());
     it.add(fixitUser);
   }
 
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 0c62134..faa3aae 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
@@ -487,8 +487,10 @@
         || successorOffset == EXCEPTIONAL_SYNC_EXIT_OFFSET) {
       return;
     }
-    // The transfer has not yet taken place, so the current position is that of the predecessor.
-    currentPosition = getCanonicalDebugPositionAtOffset(predecessorOffset);
+    // The transfer has not yet taken place, so the current position is that of the predecessor,
+    // except for exceptional edges where the transfer has already taken place.
+    currentPosition =
+        getCanonicalDebugPositionAtOffset(isExceptional ? successorOffset : predecessorOffset);
 
     LocalChangeAtOffset localChange = state.getLocalChange(predecessorOffset, successorOffset);
     if (!isExceptional) {
@@ -2874,7 +2876,7 @@
     int index = instructionIndex(offset);
     if (index < 0 || instructionCount() <= index) {
       assert false;
-      return Position.none();
+      return getPreamblePosition();
     }
     AbstractInsnNode insn = node.instructions.get(index);
     if (insn instanceof LabelNode) {
@@ -2927,10 +2929,14 @@
         }
         syntheticPosition =
             (min == Integer.MAX_VALUE)
-                ? Position.noneWithMethod(originalMethod, callerPosition)
+                ? getPreamblePosition()
                 : Position.synthetic(min < max ? min - 1 : min, originalMethod, callerPosition);
       } else {
-        syntheticPosition = Position.noneWithMethod(originalMethod, callerPosition);
+        // If in release mode we explicitly associate a synthetic none position with monitor exit.
+        // This is safe as the runtime must never throw at this position because the monitor cannot
+        // be null and the thread calling exit can only be the same thread that entered the monitor
+        // at method entry.
+        syntheticPosition = Position.syntheticNone();
       }
     }
     return syntheticPosition;
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 194558e..c20da53 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
@@ -1982,8 +1982,7 @@
             || (hasCatchHandlers && i.instructionTypeCanThrow())
             || (options.canHaveCmpIfFloatBug() && i.isCmp()));
     Instruction next = insertAt.previous();
-    instruction.forceSetPosition(
-        next.isGoto() ? next.asGoto().getTarget().getPosition() : next.getPosition());
+    instruction.forceSetPosition(next.getPosition());
     insertAt.add(instruction);
   }
 
@@ -3002,7 +3001,7 @@
       iterator.add(new InvokeVirtual(print, null, ImmutableList.of(out, indent)));
 
       // Add a block for end-of-line printing.
-      BasicBlock eol = BasicBlock.createGotoBlock(code.blocks.size());
+      BasicBlock eol = BasicBlock.createGotoBlock(code.blocks.size(), position);
       code.blocks.add(eol);
 
       BasicBlock successor = block.unlinkSingleSuccessor();
@@ -3020,9 +3019,9 @@
         BasicBlock ifBlock = BasicBlock.createIfBlock(code.blocks.size(), theIf);
         code.blocks.add(ifBlock);
         // Fallthrough block must be added right after the if.
-        BasicBlock isNullBlock = BasicBlock.createGotoBlock(code.blocks.size());
+        BasicBlock isNullBlock = BasicBlock.createGotoBlock(code.blocks.size(), position);
         code.blocks.add(isNullBlock);
-        BasicBlock isNotNullBlock = BasicBlock.createGotoBlock(code.blocks.size());
+        BasicBlock isNotNullBlock = BasicBlock.createGotoBlock(code.blocks.size(), position);
         code.blocks.add(isNotNullBlock);
 
         // Link the added blocks together.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 92bda1d..da96c4c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import it.unimi.dsi.fastutil.Hash.Strategy;
 import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
@@ -79,7 +78,6 @@
             ConstNumber canonicalizedConstant = entry.getKey().asConstNumber();
             ConstNumber newConst = ConstNumber.copyOf(code, canonicalizedConstant);
             insertCanonicalizedConstant(code, newConst);
-            newConst.setPosition(Position.none());
             for (Value outValue : entry.getValue()) {
               outValue.replaceUsers(newConst.outValue());
             }
@@ -96,6 +94,7 @@
     // instructions. It is important that the const instruction is put before any instruction
     // that can throw exceptions (since the value could be used on the exceptional edge).
     InstructionListIterator it = entryBlock.listIterator();
+    canonicalizedConstant.setPosition(entryBlock.getPosition());
     while (it.hasNext()) {
       if (!it.next().isArgument()) {
         it.previous();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index 8bb4de7..eb140c4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -6,11 +6,12 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
-import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.LiveIntervals;
@@ -18,7 +19,6 @@
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -28,6 +28,7 @@
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Objects;
 
 public class PeepholeOptimizer {
 
@@ -128,47 +129,48 @@
 
   private static BasicBlock createAndInsertBlockForSuffix(
       int blockNumber, int suffixSize, List<BasicBlock> preds, BasicBlock successorBlock) {
-    BasicBlock newBlock = BasicBlock.createGotoBlock(blockNumber);
     BasicBlock first = preds.get(0);
     assert (successorBlock != null && first.exit().isGoto())
         || (successorBlock == null && first.exit().isReturn());
-    int offsetFromEnd = successorBlock == null ? 0 : 1;
-    if (successorBlock == null) {
-      newBlock.getInstructions().removeLast();
+    BasicBlock newBlock = new BasicBlock();
+    newBlock.setNumber(blockNumber);
+    InstructionListIterator from = first.listIterator(first.getInstructions().size());
+    Int2ReferenceMap<DebugLocalInfo> newBlockEntryLocals = null;
+    if (first.getLocalsAtEntry() != null) {
+      newBlockEntryLocals = new Int2ReferenceOpenHashMap<>(first.getLocalsAtEntry());
+      int prefixSize = first.getInstructions().size() - suffixSize;
+      InstructionIterator it = first.iterator();
+      for (int i = 0; i < prefixSize; i++) {
+        Instruction instruction = it.next();
+        if (instruction.isDebugLocalsChange()) {
+          instruction.asDebugLocalsChange().apply(newBlockEntryLocals);
+        }
+      }
     }
-    InstructionListIterator from =
-        first.listIterator(first.getInstructions().size() - offsetFromEnd);
-    Int2ReferenceMap<DebugLocalInfo> newBlockEntryLocals =
-        (successorBlock == null || successorBlock.getLocalsAtEntry() == null)
-            ? new Int2ReferenceOpenHashMap<>()
-            : new Int2ReferenceOpenHashMap<>(successorBlock.getLocalsAtEntry());
     boolean movedThrowingInstruction = false;
-    for (int i = offsetFromEnd; i < suffixSize; i++) {
+    for (int i = 0; i < suffixSize; i++) {
       Instruction instruction = from.previous();
       movedThrowingInstruction = movedThrowingInstruction || instruction.instructionTypeCanThrow();
       newBlock.getInstructions().addFirst(instruction);
       instruction.setBlock(newBlock);
-      if (instruction.isDebugLocalsChange()) {
-        // Replay the debug local changes backwards to compute the entry state.
-        DebugLocalsChange change = instruction.asDebugLocalsChange();
-        for (int starting : change.getStarting().keySet()) {
-          newBlockEntryLocals.remove(starting);
-        }
-        for (Entry<DebugLocalInfo> ending : change.getEnding().int2ReferenceEntrySet()) {
-          newBlockEntryLocals.put(ending.getIntKey(), ending.getValue());
-        }
-      }
     }
     if (movedThrowingInstruction && first.hasCatchHandlers()) {
       newBlock.transferCatchHandlers(first);
     }
     for (BasicBlock pred : preds) {
+      Position lastPosition = pred.getPosition();
       LinkedList<Instruction> instructions = pred.getInstructions();
       for (int i = 0; i < suffixSize; i++) {
         instructions.removeLast();
       }
+      for (Instruction instruction : pred.getInstructions()) {
+        if (instruction.getPosition().isSome()) {
+          lastPosition = instruction.getPosition();
+        }
+      }
       Goto jump = new Goto();
       jump.setBlock(pred);
+      jump.setPosition(lastPosition);
       instructions.add(jump);
       newBlock.getPredecessors().add(pred);
       if (successorBlock != null) {
@@ -181,16 +183,37 @@
         pred.clearCatchHandlers();
       }
     }
-    newBlock.setLocalsAtEntry(newBlockEntryLocals);
+    newBlock.close(null);
+    if (newBlockEntryLocals != null) {
+      newBlock.setLocalsAtEntry(newBlockEntryLocals);
+    }
     if (successorBlock != null) {
       newBlock.link(successorBlock);
     }
     return newBlock;
   }
 
+  private static Int2ReferenceMap<DebugLocalInfo> localsAtBlockExit(BasicBlock block) {
+    if (block.getLocalsAtEntry() == null) {
+      return null;
+    }
+    Int2ReferenceMap<DebugLocalInfo> locals =
+        new Int2ReferenceOpenHashMap<>(block.getLocalsAtEntry());
+    for (Instruction instruction : block.getInstructions()) {
+      if (instruction.isDebugLocalsChange()) {
+        instruction.asDebugLocalsChange().apply(locals);
+      }
+    }
+    return locals;
+  }
+
   private static int sharedSuffixSize(
       BasicBlock block0, BasicBlock block1, RegisterAllocator allocator) {
     assert block0.exit().isGoto() || block0.exit().isReturn();
+    // If the blocks do not agree on locals at exit then they don't have any shared suffix.
+    if (!Objects.equals(localsAtBlockExit(block0), localsAtBlockExit(block1))) {
+      return 0;
+    }
     InstructionListIterator it0 = block0.listIterator(block0.getInstructions().size());
     InstructionListIterator it1 = block1.listIterator(block1.getInstructions().size());
     int suffixSize = 0;
@@ -230,6 +253,7 @@
             changed = true;
             int otherPredIndex = blockToIndex.get(wrapper);
             BasicBlock otherPred = block.getPredecessors().get(otherPredIndex);
+            assert Objects.equals(pred.getPosition(), otherPred.getPosition());
             pred.clearCatchHandlers();
             pred.getInstructions().clear();
             equivalence.clearComputedHash(pred);
@@ -242,6 +266,7 @@
             otherPred.getPredecessors().add(pred);
             Goto exit = new Goto();
             exit.setBlock(pred);
+            exit.setPosition(otherPred.getPosition());
             pred.getInstructions().add(exit);
           } else {
             blockToIndex.put(wrapper, predIndex);
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 c381944..75bf9c0 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
@@ -331,7 +331,9 @@
       Int2ReferenceMap<DebugLocalInfo> currentLocals =
           new Int2ReferenceOpenHashMap<>(openRanges.size());
       for (LocalRange openRange : openRanges) {
-        currentLocals.put(openRange.register, openRange.local);
+        if (liveLocalValues.contains(openRange.value)) {
+          currentLocals.put(openRange.register, openRange.local);
+        }
       }
 
       // Set locals at entry. This will adjust initial local registers in case of spilling.
@@ -350,7 +352,7 @@
             while (it.hasNext()) {
               LocalRange openRange = it.next();
               if (openRange.value == endAnnotation) {
-                it.remove();
+                // Don't remove the local from open-ranges as it is still technically open.
                 assert currentLocals.get(openRange.register) == openRange.local;
                 currentLocals.remove(openRange.register);
                 ending.put(openRange.register, openRange.local);
@@ -382,8 +384,9 @@
               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);
+              if (currentLocals.remove(openRange.register) != null) {
+                ending.put(openRange.register, openRange.local);
+              }
             }
           }
         }
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 8af8922..17e49a5 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -32,6 +32,7 @@
 import java.util.Deque;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
 import java.util.function.Consumer;
@@ -442,10 +443,7 @@
   }
 
   protected final JUnit3Wrapper.Command checkLine(String sourceFile, int line) {
-    return inspect(t -> {
-      Assert.assertEquals(sourceFile, t.getSourceFile());
-      Assert.assertEquals(line, t.getLineNumber());
-    });
+    return inspect(t -> t.checkLine(sourceFile, line));
   }
 
   protected final JUnit3Wrapper.Command checkInlineFrames(List<SignatureAndLine> expectedFrames) {
@@ -1043,6 +1041,8 @@
       void checkNoLocal(String localName);
       void checkLocal(String localName);
       void checkLocal(String localName, Value expectedValue);
+
+      void checkLine(String sourceFile, int line);
     }
 
     public static class DebuggeeState implements FrameInspector {
@@ -1226,6 +1226,15 @@
           checkIncorrectLocal(localName, expectedValue, localValue);
         }
 
+        @Override
+        public void checkLine(String sourceFile, int line) {
+          if (!Objects.equals(sourceFile, getSourceFile()) || line != getLineNumber()) {
+            String locationString = convertCurrentLocationToString();
+            Assert.fail(
+                "Incorrect line at " + locationString + ", expected " + sourceFile + ":" + line);
+          }
+        }
+
         /**
          * Return class name, as found in the binary. If it has not been obfuscated (minified) it's
          * identical to the original class name. Otherwise, it's the obfuscated one.
@@ -1355,6 +1364,11 @@
       }
 
       @Override
+      public void checkLine(String sourceFile, int line) {
+        getTopFrame().checkLine(sourceFile, line);
+      }
+
+      @Override
       public int getLineNumber() {
         return getTopFrame().getLineNumber();
       }
diff --git a/src/test/java/com/android/tools/r8/debug/EmptyLineAfterJoinTestRunner.java b/src/test/java/com/android/tools/r8/debug/EmptyLineAfterJoinTestRunner.java
index 5e20a21..37782dd 100644
--- a/src/test/java/com/android/tools/r8/debug/EmptyLineAfterJoinTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/EmptyLineAfterJoinTestRunner.java
@@ -10,7 +10,6 @@
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.Collection;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
@@ -59,8 +58,6 @@
 
   @Test
   public void test() throws Throwable {
-    Assume.assumeFalse(
-        "b/110873173 Fails because of missing line 16", (config instanceof D8DebugTestConfig));
     runDebugTest(
         config,
         NAME,
diff --git a/src/test/java/com/android/tools/r8/debug/LocalEndTest.java b/src/test/java/com/android/tools/r8/debug/LocalEndTest.java
index 036e590..f4a3413 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalEndTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTest.java
@@ -17,11 +17,22 @@
     System.out.println(y);
   }
 
-  private void bar() {
-    // nothing to do
+  public void bar() {
+    if (raise) {
+      System.out.print("throwing ");
+      throw new RuntimeException();
+    }
+    System.out.print("not-throwing ");
+  }
+
+  public final boolean raise;
+
+  public LocalEndTest(boolean raise) {
+    this.raise = raise;
   }
 
   public static void main(String[] args) {
-    new LocalEndTest().foo();
+    new LocalEndTest(false).foo();
+    new LocalEndTest(true).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
index ad6737c..3111779 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalEndTestDump.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTestDump.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.debug;
 
 import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
 import org.objectweb.asm.Opcodes;
@@ -13,6 +14,7 @@
   public static byte[] dump() {
 
     ClassWriter cw = new ClassWriter(0);
+    FieldVisitor fv;
     MethodVisitor mv;
 
     cw.visit(
@@ -26,19 +28,8 @@
     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();
+      fv = cw.visitField(ACC_PUBLIC + ACC_FINAL, "raise", "Z", null, null);
+      fv.visitEnd();
     }
     {
       mv = cw.visitMethod(ACC_PUBLIC, "foo", "()V", null, null);
@@ -57,7 +48,7 @@
       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);
+          INVOKEVIRTUAL, "com/android/tools/r8/debug/LocalEndTest", "bar", "()V", false);
       Label l4 = new Label();
       mv.visitLabel(l4);
       mv.visitLineNumber(13, l4);
@@ -76,10 +67,8 @@
           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,
@@ -107,38 +96,61 @@
       mv.visitEnd();
     }
     {
-      mv = cw.visitMethod(ACC_PRIVATE, "bar", "()V", null, null);
+      mv = cw.visitMethod(ACC_PUBLIC, "bar", "()V", null, null);
       mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitFieldInsn(GETFIELD, "com/android/tools/r8/debug/LocalEndTest", "raise", "Z");
       Label l0 = new Label();
+      mv.visitJumpInsn(IFEQ, l0);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitLdcInsn("throwing ");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
+      mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
+      mv.visitInsn(DUP);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "()V", false);
+      mv.visitInsn(ATHROW);
       mv.visitLabel(l0);
-      mv.visitLineNumber(22, l0);
+      mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      mv.visitLdcInsn("not-throwing ");
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)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(0, 1);
+      mv.visitMaxs(2, 1);
+      mv.visitEnd();
+    }
+    {
+      mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Z)V", null, null);
+      mv.visitCode();
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      mv.visitVarInsn(ALOAD, 0);
+      mv.visitVarInsn(ILOAD, 1);
+      mv.visitFieldInsn(PUTFIELD, "com/android/tools/r8/debug/LocalEndTest", "raise", "Z");
+      mv.visitInsn(RETURN);
+      mv.visitMaxs(2, 2);
       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.visitInsn(ICONST_0);
       mv.visitMethodInsn(
-          INVOKESPECIAL, "com/android/tools/r8/debug/LocalEndTest", "<init>", "()V", false);
+          INVOKESPECIAL, "com/android/tools/r8/debug/LocalEndTest", "<init>", "(Z)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.visitTypeInsn(NEW, "com/android/tools/r8/debug/LocalEndTest");
+      mv.visitInsn(DUP);
+      mv.visitInsn(ICONST_1);
+      mv.visitMethodInsn(
+          INVOKESPECIAL, "com/android/tools/r8/debug/LocalEndTest", "<init>", "(Z)V", false);
+      mv.visitMethodInsn(
+          INVOKEVIRTUAL, "com/android/tools/r8/debug/LocalEndTest", "foo", "()V", false);
       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.visitMaxs(3, 1);
       mv.visitEnd();
     }
     cw.visitEnd();
diff --git a/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java b/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java
index 3f86969..af89b16 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalEndTestRunner.java
@@ -10,6 +10,7 @@
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.Collection;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
@@ -54,16 +55,33 @@
 
   @Test
   public void test() throws Throwable {
+    Value xInitial = Value.createInt(42);
+    Value xNormal = Value.createInt(7);
+    Value xExceptional = xInitial;
     runDebugTest(
         config,
         NAME,
-        breakpoint(NAME, "foo", 13),
+        breakpoint(NAME, "foo", 12),
+        // Run to breakpoint for the non-throwing case.
         run(),
+        checkLine(FILE, 12),
+        checkLocal("x", xInitial),
+        stepOver(),
         checkLine(FILE, 13),
-        checkLocal("x"),
+        checkLocal("x", xInitial),
         stepOver(),
         checkLine(FILE, 14),
-        checkLocal("x"),
+        checkLocal("x", xNormal),
+        stepOver(),
+        checkLine(FILE, 16),
+        checkNoLocal("x"),
+        // Run to breakpoint for the throwing case.
+        run(),
+        checkLine(FILE, 12),
+        checkLocal("x", xInitial),
+        stepOver(),
+        checkLine(FILE, 14),
+        checkLocal("x", xExceptional),
         stepOver(),
         checkLine(FILE, 16),
         checkNoLocal("x"),
diff --git a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
index c8e6e41..cd9e19d 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/SynchronizedMethodTestRunner.java
@@ -103,5 +103,7 @@
     // have an associated line.
     inspector.checkStartLine(9);
     inspector.checkNoLine(8);
+    // Also ensure we did not emit a preamble position at line zero.
+    inspector.checkNoLine(0);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
index 9fb4702..2b92ab1 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
@@ -67,56 +67,57 @@
     // is needed and the value 10 is *not* still in register 0 at that point.
     BasicBlock block = new BasicBlock();
     block.setNumber(0);
+    Position position = Position.testingPosition();
 
     Value v3 = new Value(3, ValueType.LONG, null);
     v3.setNeedsRegister(true);
     new MockLiveIntervals(v3);
     Instruction instruction = new ConstNumber(v3, 0);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     Value v0 = new Value(0, ValueType.LONG, null);
     v0.setNeedsRegister(true);
     new MockLiveIntervals(v0);
     instruction = new ConstNumber(v0, 10);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     instruction = new Div(NumericType.LONG, v3, v3, v0);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     Value v2 = new Value(2, ValueType.INT, null);
     v2.setNeedsRegister(true);
     new MockLiveIntervals(v2);
     instruction = new ConstNumber(v2, 10);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     Value v1 = new Value(1, ValueType.INT, null);
     v1.setNeedsRegister(true);
     new MockLiveIntervals(v1);
     instruction = new Move(v1 ,v2);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     instruction = new Div(NumericType.INT, v1, v1, v1);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     Value v0_2 = new Value(0, ValueType.LONG, null);
     v0_2.setNeedsRegister(true);
     new MockLiveIntervals(v0_2);
     instruction = new ConstNumber(v0_2, 10);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     instruction = new Div(NumericType.LONG, v3, v3, v0_2);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block.add(instruction);
 
     Instruction ret = new Return();
-    ret.setPosition(Position.none());
+    ret.setPosition(position);
     block.add(ret);
     block.setFilledForTesting();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
index a095615..6470876 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueNumberGenerator;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import java.util.LinkedList;
 import org.junit.Test;
@@ -36,23 +37,24 @@
     //   throw v0
     // block2:
     //   return
+    Position position = Position.testingPosition();
     BasicBlock block2 = new BasicBlock();
     block2.setNumber(2);
-    BasicBlock block0 = BasicBlock.createGotoBlock(0, block2);
+    BasicBlock block0 = BasicBlock.createGotoBlock(0, position, block2);
     block0.setFilledForTesting();
     block2.getPredecessors().add(block0);
     Instruction ret = new Return();
-    ret.setPosition(Position.none());
+    ret.setPosition(position);
     block2.add(ret);
     block2.setFilledForTesting();
     BasicBlock block1 = new BasicBlock();
     block1.setNumber(1);
     Value value = new Value(0, ValueType.INT, null);
     Instruction number = new ConstNumber(value, 0);
-    number.setPosition(Position.none());
+    number.setPosition(position);
     block1.add(number);
     Instruction throwing = new Throw(value);
-    throwing.setPosition(Position.none());
+    throwing.setPosition(position);
     block1.add(throwing);
     block1.setFilledForTesting();
     LinkedList<BasicBlock> blocks = new LinkedList<>();
@@ -62,7 +64,8 @@
     // Check that the goto in block0 remains. There was a bug in the trivial goto elimination
     // that ended up removing that goto changing the code to start with the unreachable
     // throw.
-    IRCode code = new IRCode(null, null, blocks, new ValueNumberGenerator(), false, false);
+    IRCode code =
+        new IRCode(new InternalOptions(), null, blocks, new ValueNumberGenerator(), false, false);
     CodeRewriter.collapsTrivialGotos(null, code);
     assertTrue(code.blocks.get(0).isTrivialGoto());
     assertTrue(blocks.contains(block0));
@@ -85,22 +88,23 @@
     //
     // block3:
     //   goto block3
+    Position position = Position.testingPosition();
     BasicBlock block2 = new BasicBlock();
     block2.setNumber(2);
     Instruction ret = new Return();
-    ret.setPosition(Position.none());
+    ret.setPosition(position);
     block2.add(ret);
     block2.setFilledForTesting();
 
     BasicBlock block3 = new BasicBlock();
     block3.setNumber(3);
     Instruction instruction = new Goto();
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block3.add(instruction);
     block3.setFilledForTesting();
     block3.getSuccessors().add(block3);
 
-    BasicBlock block1 = BasicBlock.createGotoBlock(1);
+    BasicBlock block1 = BasicBlock.createGotoBlock(1, position);
     block1.getSuccessors().add(block3);
     block1.setFilledForTesting();
 
@@ -108,10 +112,10 @@
     block0.setNumber(0);
     Value value = new Value(0, ValueType.OBJECT, null);
     instruction = new Argument(value);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block0.add(instruction);
     instruction = new If(Type.EQ, value);
-    instruction.setPosition(Position.none());
+    instruction.setPosition(position);
     block0.add(instruction);
     block0.getSuccessors().add(block2);
     block0.getSuccessors().add(block1);
@@ -130,7 +134,8 @@
     // Check that the goto in block0 remains. There was a bug in the trivial goto elimination
     // that ended up removing that goto changing the code to start with the unreachable
     // throw.
-    IRCode code = new IRCode(null, null, blocks, new ValueNumberGenerator(), false, false);
+    IRCode code =
+        new IRCode(new InternalOptions(), null, blocks, new ValueNumberGenerator(), false, false);
     CodeRewriter.collapsTrivialGotos(null, code);
     assertTrue(block0.getInstructions().get(1).isIf());
     assertEquals(block1, block0.getInstructions().get(1).asIf().fallthroughBlock());