Compute local info at exceptional blocks based on its predecessor.

This CL ends the visibility of locals that are clobbered as part of spill-moves
in the header of an exceptional block. Also added regression test that observed
incorrect locals information on entry to an exceptional block.

R=ager

Change-Id: I50459fe34cf4ebc1c378e3ddfadba6a4b1de74cf
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 16bc790..44721ae 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
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.code;
 
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.DexBuilder;
@@ -378,6 +379,18 @@
     return instructions.get(instructions.size() - 1).asJumpInstruction();
   }
 
+  public Instruction exceptionalExit() {
+    assert hasCatchHandlers();
+    ListIterator<Instruction> it = listIterator(instructions.size());
+    while (it.hasPrevious()) {
+      Instruction instruction = it.previous();
+      if (instruction.instructionTypeCanThrow()) {
+        return instruction;
+      }
+    }
+    throw new Unreachable();
+  }
+
   public void clearUserInfo() {
     phis = null;
     instructions.forEach(Instruction::clearUserInfo);
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 a08b044..0a1cc1b 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
@@ -18,6 +18,7 @@
   public DebugLocalsChange(
       Int2ReferenceMap<DebugLocalInfo> ending, Int2ReferenceMap<DebugLocalInfo> starting) {
     super(null);
+    assert !ending.isEmpty() || !starting.isEmpty();
     this.ending = ending;
     this.starting = starting;
   }
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 1e45d52..3a77a52 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
@@ -29,9 +29,10 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Multiset;
-import com.google.common.collect.Multiset.Entry;
 import com.google.common.collect.Multisets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
 import it.unimi.dsi.fastutil.ints.IntArraySet;
 import it.unimi.dsi.fastutil.ints.IntIterator;
@@ -75,6 +76,7 @@
   }
 
   private static class LocalRange implements Comparable<LocalRange> {
+    final Value value;
     final DebugLocalInfo local;
     final int register;
     final int start;
@@ -82,6 +84,7 @@
 
     LocalRange(Value value, int register, int start, int end) {
       assert value.getLocalInfo() != null;
+      this.value = value;
       this.local = value.getLocalInfo();
       this.register = register;
       this.start = start;
@@ -252,7 +255,7 @@
           // 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 = getRegisterForValue(value, value.isArgument() ? 0 : start);
+          int register = getArgumentOrAllocateRegisterForValue(value, start);
           ranges.add(new LocalRange(value, register, start, nextEnd));
           Integer nextStart = nextInRange(nextEnd, end, starts);
           if (nextStart == null) {
@@ -262,7 +265,8 @@
           start = nextStart;
         }
         if (start >= 0) {
-          ranges.add(new LocalRange(value, getRegisterForValue(value, start), start, end));
+          ranges.add(new LocalRange(value, getArgumentOrAllocateRegisterForValue(value, start),
+              start, end));
         }
       }
     }
@@ -287,7 +291,7 @@
     }
 
     for (BasicBlock block : blocks) {
-      block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(currentLocals));
+      boolean blockEntry = true;
       ListIterator<Instruction> instructionIterator = block.listIterator();
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
@@ -316,15 +320,25 @@
           }
           nextStartingRange = rangeIterator.hasNext() ? rangeIterator.next() : null;
         }
-        if (localsChanged && shouldEmitChangesAtInstruction(instruction)) {
-          DebugLocalsChange change = createLocalsChange(ending, starting);
-          if (change != null) {
-            if (instruction.isDebugPosition() || instruction.isJumpInstruction()) {
-              instructionIterator.previous();
-              instructionIterator.add(new DebugLocalsChange(ending, starting));
-              instructionIterator.next();
-            } else {
-              instructionIterator.add(new DebugLocalsChange(ending, starting));
+
+        if (blockEntry) {
+          blockEntry = false;
+          if (instruction.isMoveException()) {
+            fixupSpillMovesAtMoveException(block, instructionIterator, openRanges, currentLocals);
+          } else {
+            block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(currentLocals));
+          }
+        } else {
+          if (localsChanged && shouldEmitChangesAtInstruction(instruction)) {
+            DebugLocalsChange change = createLocalsChange(ending, starting);
+            if (change != null) {
+              if (instruction.isDebugPosition() || instruction.isJumpInstruction()) {
+                instructionIterator.previous();
+                instructionIterator.add(change);
+                instructionIterator.next();
+              } else {
+                instructionIterator.add(change);
+              }
             }
           }
         }
@@ -333,13 +347,83 @@
     }
   }
 
+  private void fixupSpillMovesAtMoveException(
+      BasicBlock block,
+      ListIterator<Instruction> instructionIterator,
+      List<LocalRange> openRanges,
+      Int2ReferenceMap<DebugLocalInfo> finalLocals) {
+    Int2ReferenceMap<DebugLocalInfo> initialLocals = new Int2ReferenceOpenHashMap<>();
+    int exceptionalIndex = block.getPredecessors().get(0).exceptionalExit().getNumber();
+    for (LocalRange open : openRanges) {
+      int exceptionalRegister = getArgumentOrAllocateRegisterForValue(open.value, exceptionalIndex);
+      initialLocals.put(exceptionalRegister, open.local);
+    }
+    block.setLocalsAtEntry(new Int2ReferenceOpenHashMap<>(initialLocals));
+    Int2ReferenceMap<DebugLocalInfo> clobberedLocals = new Int2ReferenceOpenHashMap<>();
+    Iterator<Instruction> moveIterator = block.iterator();
+    assert block.entry().isMoveException();
+    int index = block.entry().getNumber();
+    moveIterator.next();
+    while (moveIterator.hasNext()) {
+      Instruction next = moveIterator.next();
+      if (next.getNumber() != -1) {
+        break;
+      }
+      if (clobberedLocals.isEmpty()) {
+        // Advance the iterator so it ends up at the first move that clobbers a local.
+        instructionIterator.next();
+      }
+      if (next.isMove()) {
+        Move move = next.asMove();
+        int dstRegister = getArgumentOrAllocateRegisterForValue(move.dest(), index);
+        DebugLocalInfo dstInitialLocal = initialLocals.get(dstRegister);
+        DebugLocalInfo dstFinalLocal = finalLocals.get(dstRegister);
+        if (dstInitialLocal != null && dstInitialLocal != dstFinalLocal) {
+          initialLocals.remove(dstRegister);
+          clobberedLocals.put(dstRegister, dstInitialLocal);
+        }
+      }
+    }
+    // Add an initial local change for all clobbered locals after the first clobbered local.
+    if (!clobberedLocals.isEmpty()) {
+      instructionIterator.add(new DebugLocalsChange(
+          clobberedLocals, Int2ReferenceMaps.emptyMap()));
+    }
+    // Compute the final change in locals and emit it after all spill moves.
+    while (instructionIterator.hasNext()) {
+      if (instructionIterator.next().getNumber() != -1) {
+        instructionIterator.previous();
+        break;
+      }
+    }
+    Int2ReferenceMap<DebugLocalInfo> ending = new Int2ReferenceOpenHashMap<>();
+    Int2ReferenceMap<DebugLocalInfo> starting = new Int2ReferenceOpenHashMap<>();
+    for (Entry<DebugLocalInfo> initialLocal : initialLocals.int2ReferenceEntrySet()) {
+      if (finalLocals.get(initialLocal.getIntKey()) != initialLocal.getValue()) {
+        ending.put(initialLocal.getIntKey(), initialLocal.getValue());
+      }
+    }
+    for (Entry<DebugLocalInfo> finalLocal : finalLocals.int2ReferenceEntrySet()) {
+      if (initialLocals.get(finalLocal.getIntKey()) != finalLocal.getValue()) {
+        starting.put(finalLocal.getIntKey(), finalLocal.getValue());
+      }
+    }
+    DebugLocalsChange change = createLocalsChange(ending, starting);
+    if (change != null) {
+      instructionIterator.add(change);
+    }
+  }
+
   private DebugLocalsChange createLocalsChange(
       Int2ReferenceMap<DebugLocalInfo> ending, Int2ReferenceMap<DebugLocalInfo> starting) {
+    if (ending.isEmpty() && starting.isEmpty()) {
+      return null;
+    }
     if (ending.isEmpty() || starting.isEmpty()) {
       return new DebugLocalsChange(ending, starting);
     }
     IntSet unneeded = new IntArraySet(Math.min(ending.size(), starting.size()));
-    for (Int2ReferenceMap.Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) {
+    for (Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) {
       if (starting.get(entry.getIntKey()) == entry.getValue()) {
         unneeded.add(entry.getIntKey());
       }
@@ -1112,7 +1196,7 @@
           map.add(operandRegister);
         }
       }
-      for (Entry<Integer> entry : Multisets.copyHighestCountFirst(map).entrySet()) {
+      for (Multiset.Entry<Integer> entry : Multisets.copyHighestCountFirst(map).entrySet()) {
         int register = entry.getElement();
         if (tryHint(unhandledInterval, registerConstraint, freePositions, needsRegisterPair,
             register)) {
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 be25097..6a5f738 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -325,6 +325,10 @@
     return new JUnit3Wrapper.Command.SetLocalCommand(localName, newValue);
   }
 
+  protected final JUnit3Wrapper.Command getLocal(String localName, Consumer<Value> inspector) {
+    return t -> inspector.accept(t.debuggeeState.getLocalValues().get(localName));
+  }
+
   @Ignore("Prevents Gradle from running the wrapper as a test.")
   static class JUnit3Wrapper extends JDWPTestCase {
 
@@ -643,7 +647,8 @@
           assert valuesCount == 1;
           Value localValue = replyPacket.getNextValueAsValue();
 
-          Assert.assertEquals(expectedValue, localValue);
+          Assert.assertEquals("Incorrect value for local '" + localName + "'",
+              expectedValue, localValue);
         }
 
         public String getClassName() {
diff --git a/src/test/java/com/android/tools/r8/debug/LocalsTest.java b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
index 6a6a20f..1fd4bc9 100644
--- a/src/test/java/com/android/tools/r8/debug/LocalsTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LocalsTest.java
@@ -317,4 +317,35 @@
         run());
   }
 
+  @Test
+  public void testInvokeRangeLongThrowOnDiv() throws Throwable {
+    final int initialValueOfX = 21;
+    final long expectedValueOfL = (long) initialValueOfX * 2;
+    runDebugTest("Locals",
+        breakpoint("Locals", "foo"),
+        run(),
+        // Initialize obj to 42 using original value of x.
+        stepOver(),
+        // Set value of x to zero which will cause a div-by-zero arithmetic exception below.
+        checkLocal("x", Value.createInt(initialValueOfX)),
+        setLocal("x", Value.createInt(0)),
+        // Single step until the catch handler triggers.
+        checkLine(SOURCE_FILE, 166), stepOver(),
+        checkLine(SOURCE_FILE, 168), stepOver(),
+        checkLine(SOURCE_FILE, 169), stepOver(),
+        // At the catch handler, inspect the initial state of locals.
+        checkLine(SOURCE_FILE, 172),
+        checkLocal("x", Value.createInt(0)),
+        getLocal("obj", value -> Assert.assertEquals(Tag.OBJECT_TAG, value.getTag())),
+        checkLocal("l", Value.createLong(expectedValueOfL)),
+        // Step onto first line of catch handler and inspect again, including the exception local.
+        stepOver(),
+        checkLine(SOURCE_FILE, 173),
+        getLocal("e", value -> Assert.assertEquals(Tag.OBJECT_TAG, value.getTag())),
+        checkLocal("x", Value.createInt(0)),
+        getLocal("obj", value -> Assert.assertEquals(Tag.OBJECT_TAG, value.getTag())),
+        checkLocal("l", Value.createLong(expectedValueOfL)),
+        run());
+  }
+
 }