Avoid conflating register sets at move-exception

In-resolution moves to a move-exception instruction cannot be inserted before the move-exception instruction.

Previously, we worked around this by adding the in-resolution moves to a move-exception instruction as in-resolution moves to the goto instruction that is always after the move-exception instruction (since catch handlers are split blocks).

The in-resolution moves to the move-exception instruction must be inserted before any of the in-resolution moves to the goto instruction, however. By conflating the two in-resolution move sets, the subsequent parallel move scheduling may incorrectly reorder two moves.

Example: Consider that the in-resolution moves for the move-exception instruction is the set { move r11, r3 } and the in-resolution moves for the goto instruction is the set { move r1, r11 }.

The instructions must be laid out in sequence as:

  move r11, r3
  move r1, r11

When the two sets are conflated as { move r11, r3; move r1, r11 }, parallel move scheduling will instead layout the moves as follows, which may lead to arbitrary errors:

  move r1, r11
  move r11, r3

Fix: Instead of conflating the two in-resolution move sets, this CL simply inserts the in-resolution moves for the move-exception instruction immediately after the move-exception instruction.

Bug: b/293501981
Change-Id: I3975902ce63fb1f03914441c7c7357c839ff8a74
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 b66dd62..b65fb0c 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
@@ -2499,8 +2499,6 @@
           if (fromIntervals != toIntervals) {
             if (block.exit().isGoto() && !isCatch) {
               spillMoves.addOutResolutionMove(fromInstruction - 1, toIntervals, fromIntervals);
-            } else if (successor.entry().isMoveException()) {
-              spillMoves.addInResolutionMove(toInstruction + 1, toIntervals, fromIntervals);
             } else {
               spillMoves.addInResolutionMove(toInstruction - 1, toIntervals, fromIntervals);
             }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
index 1c13ae6..ce73040 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
@@ -13,7 +13,9 @@
 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.utils.MapUtils;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
@@ -67,46 +69,7 @@
     assert to.getSplitParent() == from.getSplitParent();
     BasicBlock atEntryToBlock = blockStartMap.get(i + 1);
     if (atEntryToBlock == null) {
-      Value value = from.getValue();
-      if (value.definition != null
-          && value.definition.isMoveException()
-          && to.getStart() == value.definition.asMoveException().getNumber() + 1) {
-        // Consider the following IR code.
-        //   40: ...
-        //   42: v0 <- move-exception
-        //   44: ...
-        //   ...
-        //   50: ... v0 ... // 4-bit constrained use of v0
-        //
-        // Initially, the liveness interval of v0 is [42; 50[. If the method has overlapping move-
-        // exception intervals (and the register allocator needs more than 16 registers), then the
-        // intervals of v0 will be split immediately after its definition. Therefore, v0 will have
-        // two liveness intervals: I1=[42; 43[ and I2=[43;50[.
-        //
-        // When allocating a register for the interval I2, we may need to spill an existing value v1
-        // that is currently active. As a result, we will split the liveness interval of v1 before
-        // the start of I2 (i.e., at position 43). The live intervals of v1 after the split
-        // therefore becomes J1=[x, y[, J2=[41, 43[, J3=[43, z[.
-        //
-        // If the registers assigned to J1 and J2 are different, we will create an in-move at
-        // position 43 in resolveControlFlow() (not position 41, since the first instruction of the
-        // target block is a move-exception instruction). If the the registers assigned to I1 and I2
-        // are also different, then an in-move will also be created at position 43 by the call to
-        // addSpillOrRestoreMove() in insertMoves().
-        //
-        // If the registers of I2 and J2 are the same (which is a valid assignment), then we will
-        // do parallel move scheduling for the following two in-moves:
-        //   move X, reg(I2)
-        //   move X, reg(J2)
-        //
-        // Therefore, there is a risk that we end up with the value that has been spilled instead of
-        // the exception object in register X at position 44. To avoid this situation, we schedule
-        // the move of the exception object (in this case, "move X, reg(I2)") as an out-move, such
-        // that it always gets inserted *after* the resolution moves of the current block.
-        addOutMove(i, to, from);
-      } else {
-        addInMove(i, to, from);
-      }
+      addInMove(i, to, from);
     }
   }
 
@@ -177,21 +140,21 @@
             argumentValue != null;
             argumentValue = argumentValue.getNextConsecutive()) {
           Instruction instruction = argumentValue.definition;
-          int number = instruction.getNumber();
-          if (needsMovesBeforeInstruction(number)) {
+          if (needsMovesBeforeInstruction(instruction)) {
             scheduleMovesBeforeInstruction(tempRegister, instruction, insertAt);
           }
         }
       }
 
-      while (insertAt.hasNext()) {
-        Instruction instruction = insertAt.peekNext();
-        assert !instruction.isArgument();
-        int number = instruction.getNumber();
-        if (needsMovesBeforeInstruction(number)) {
-          scheduleMovesBeforeInstruction(tempRegister, instruction, insertAt);
+      while (true) {
+        Instruction instruction = insertAt.nextUntil(this::needsMovesBeforeInstruction);
+        if (instruction == null) {
+          break;
         }
-        insertAt.next();
+        if (!instruction.isMoveException()) {
+          insertAt.previous();
+        }
+        scheduleMovesBeforeInstruction(tempRegister, instruction, insertAt);
       }
     }
     return usedTempRegisters;
@@ -209,7 +172,8 @@
     return toType;
   }
 
-  private boolean needsMovesBeforeInstruction(int i) {
+  private boolean needsMovesBeforeInstruction(Instruction instruction) {
+    int i = instruction.getNumber();
     return instructionToOutMoves.containsKey(i - 1)
         || instructionToInMoves.containsKey(i - 1)
         || instructionToPhiMoves.containsKey(i - 1);
@@ -281,6 +245,8 @@
 
   private void scheduleMovesBeforeInstruction(
       int tempRegister, Instruction instruction, InstructionListIterator insertAt) {
+    assert needsMovesBeforeInstruction(instruction);
+
     int number = instruction.getNumber();
 
     Position position;
@@ -297,27 +263,34 @@
 
     // Spill and restore moves for the incoming edge.
     Set<SpillMove> inMoves =
-        instructionToInMoves.computeIfAbsent(number - 1, (k) -> new LinkedHashSet<>());
+        MapUtils.removeOrDefault(instructionToInMoves, number - 1, Collections.emptySet());
     removeArgumentRestores(inMoves);
 
     // Spill and restore moves for the outgoing edge.
     Set<SpillMove> outMoves =
-        instructionToOutMoves.computeIfAbsent(number - 1, (k) -> new LinkedHashSet<>());
+        MapUtils.removeOrDefault(instructionToOutMoves, number - 1, Collections.emptySet());
     removeArgumentRestores(outMoves);
 
     // Get the phi moves for this instruction and schedule them with the out going spill moves.
     Set<SpillMove> phiMoves =
-        instructionToPhiMoves.computeIfAbsent(number - 1, (k) -> new LinkedHashSet<>());
+        MapUtils.removeOrDefault(instructionToPhiMoves, number - 1, Collections.emptySet());
 
     // Remove/rewrite moves that we can guarantee will not be needed.
     pruneParallelMoveSets(inMoves, outMoves, phiMoves);
 
     // Schedule out and phi moves together.
-    outMoves.addAll(phiMoves);
+    if (outMoves.isEmpty()) {
+      // Note outMoves can be an empty immutable set, so we can't addAll.
+      outMoves = phiMoves;
+    } else {
+      outMoves.addAll(phiMoves);
+    }
 
     // Perform parallel move scheduling independently for the in and out moves.
     scheduleMoves(tempRegister, inMoves, insertAt, position);
     scheduleMoves(tempRegister, outMoves, insertAt, position);
+
+    assert !needsMovesBeforeInstruction(instruction);
   }
 
   // Remove restore moves that restore arguments. Since argument register reuse is