Refactor user replacement to avoid concurrent modification errors.

R=ager

Change-Id: I9fdc4a457a82b9b0a2f84125638e04801e306313
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 13325d8..e99dbf1 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
@@ -14,9 +14,11 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.StringUtils.BraceType;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 public abstract class Instruction {
 
@@ -24,7 +26,7 @@
   protected final List<Value> inValues = new ArrayList<>();
   private BasicBlock block = null;
   private int number = -1;
-  private List<Value> debugValues = null;
+  private Set<Value> debugValues = null;
 
   protected Instruction(Value outValue) {
     setOutValue(outValue);
@@ -70,7 +72,7 @@
   public void addDebugValue(Value value) {
     assert value.getLocalInfo() != null;
     if (debugValues == null) {
-      debugValues = new ArrayList<>();
+      debugValues = new HashSet<>();
     }
     debugValues.add(value);
     value.addDebugUser(this);
@@ -92,39 +94,25 @@
 
   public abstract void buildDex(DexBuilder builder);
 
-  public void replaceValue(Value oldValue, Value newValue) {
+  public void replaceValue(Value oldValue, Value newValue, List<Instruction> toRemove) {
     for (int i = 0; i < inValues.size(); i++) {
       if (oldValue == inValues.get(i)) {
         inValues.set(i, newValue);
         newValue.addUser(this);
-        oldValue.removeUser(this);
+        toRemove.add(this);
       }
     }
   }
 
-  // Similar to Phi::replaceTrivialPhi, removal can cause a concurrent modification error.
-  // TODO(ager): Consider unifying with other replace methods and avoid the C.M.E.
-  public boolean replaceDebugValue(Value oldValue, Value newValue) {
-    if (debugValues == null) {
-      return false;
-    }
-    int found = -1;
-    for (int i = 0; i < debugValues.size(); i++) {
-      if (oldValue == debugValues.get(i)) {
-        assert found == -1;
-        found = i;
-        if (newValue.getLocalInfo() != null) {
-          // TODO(zerny): Insert a write if replacing a phi with different debug-local info.
-          debugValues.set(i, newValue);
-          newValue.addDebugUser(this);
-        }
+  public void replaceDebugValue(Value oldValue, Value newValue, List<Instruction> toRemove) {
+    if (debugValues.remove(oldValue)) {
+      toRemove.add(this);
+      if (newValue.getLocalInfo() != null) {
+        // TODO(zerny): Insert a write if replacing a phi with different debug-local info.
+        addDebugValue(newValue);
       }
+      // TODO(zerny): Else: Insert a write if replacing a phi with associated debug-local info.
     }
-    if (found >= 0 && newValue.getLocalInfo() == null) {
-      // TODO(zerny): Insert a write if replacing a phi with associated debug-local info.
-      debugValues.remove(found);
-    }
-    return found >= 0;
   }
 
   /**
@@ -324,8 +312,8 @@
     return outValue == null ? null : outValue.getLocalInfo();
   }
 
-  public List<Value> getDebugValues() {
-    return debugValues != null ? debugValues : ImmutableList.of();
+  public Set<Value> getDebugValues() {
+    return debugValues != null ? debugValues : ImmutableSet.of();
   }
 
   public boolean isArrayGet() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 185d2fd..96069bf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -10,18 +10,19 @@
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 public class Phi extends Value {
 
   private final BasicBlock block;
   private final List<Value> operands = new ArrayList<>();
-  private List<Value> debugValues = null;
+  private Set<Value> debugValues = null;
 
   // Trivial phis are eliminated during IR construction. When a trivial phi is eliminated
   // we need to update all references to it. A phi can be referenced from phis, instructions
@@ -101,7 +102,7 @@
   public void addDebugValue(Value value) {
     assert value.getLocalInfo() != null;
     if (debugValues == null) {
-      debugValues = new ArrayList<>();
+      debugValues = new HashSet<>();
     }
     debugValues.add(value);
     value.addDebugPhiUser(this);
@@ -154,31 +155,22 @@
     current.removePhiUser(this);
   }
 
-  // Removing the phi user from the current value leads to concurrent modification errors
-  // during trivial phi elimination. It is safe to not remove the phi user from current
-  // since current will be unreachable after trivial phi elimination.
-  // TODO(ager): can we unify the these replace methods and avoid the concurrent modification
-  // issue?
-  private void replaceTrivialPhi(Value current, Value newValue) {
+  void replaceTrivialPhi(Value current, Value newValue, List<Phi> toRemove) {
     for (int i = 0; i < operands.size(); i++) {
       if (operands.get(i) == current) {
         operands.set(i, newValue);
         newValue.addPhiUser(this);
+        toRemove.add(this);
       }
     }
   }
 
-  private void replaceTrivialDebugPhi(Value current, Value newValue) {
-    if (debugValues == null) {
-      return;
-    }
+  void replaceTrivialDebugPhi(Value current, Value newValue, List<Phi> toRemove) {
     assert current.getLocalInfo() != null;
     assert current.getLocalInfo() == newValue.getLocalInfo();
-    for (int i = 0; i < debugValues.size(); i++) {
-      if (debugValues.get(i) == current) {
-        debugValues.set(i, newValue);
-        newValue.addDebugPhiUser(this);
-      }
+    if (debugValues.remove(current)) {
+      addDebugValue(newValue);
+      toRemove.add(this);
     }
   }
 
@@ -213,35 +205,17 @@
       same = op;
     }
     assert isTrivialPhi();
+    assert same != null : "ill-defined phi";
     // Removing this phi, so get rid of it as a phi user from all of the operands to avoid
     // recursively getting back here with the same phi. If the phi has itself as an operand
     // that also removes the self-reference.
     for (Value op : operands) {
       op.removePhiUser(this);
     }
-    // Replace this phi with the unique value in all users.
-    for (Instruction user : uniqueUsers()) {
-      user.replaceValue(this, same);
-    }
-    for (Phi user : uniquePhiUsers()) {
-      user.replaceTrivialPhi(this, same);
-    }
-    if (debugUsers() != null) {
-      List<Instruction> removed = new ArrayList<>();
-      for (Instruction user : debugUsers()) {
-        if (user.replaceDebugValue(this, same)) {
-          removed.add(user);
-        }
-      }
-      removed.forEach(this::removeDebugUser);
-      for (Phi user : debugPhiUsers()) {
-        user.replaceTrivialDebugPhi(this, same);
-      }
-    }
     // If IR construction is taking place, update the definition users.
     if (definitionUsers != null) {
       for (Map<Integer, Value> user : definitionUsers) {
-        for (Map.Entry<Integer, Value> entry : user.entrySet()) {
+        for (Entry<Integer, Value> entry : user.entrySet()) {
           if (entry.getValue() == this) {
             entry.setValue(same);
             if (same.isPhi()) {
@@ -251,9 +225,14 @@
         }
       }
     }
-    // Try to simplify phi users that might now have become trivial.
-    for (Phi user : uniquePhiUsers()) {
-      user.removeTrivialPhi();
+    {
+      Set<Phi> phiUsersToSimplify = uniquePhiUsers();
+      // Replace this phi with the unique value in all users.
+      replaceInUsers(same);
+      // Try to simplify phi users that might now have become trivial.
+      for (Phi user : phiUsersToSimplify) {
+        user.removeTrivialPhi();
+      }
     }
     // Get rid of the phi itself.
     block.removePhi(this);
@@ -349,7 +328,7 @@
     return true;
   }
 
-  public List<Value> getDebugValues() {
-    return debugValues != null ? debugValues : ImmutableList.of();
+  public Set<Value> getDebugValues() {
+    return debugValues != null ? debugValues : ImmutableSet.of();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 78cb4a5..07f6913 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -308,27 +308,52 @@
     }
     if (debugData != null) {
       for (Instruction user : debugUsers()) {
-        user.getDebugValues().replaceAll(v -> {
-          if (v == this) {
-            newValue.addDebugUser(user);
-            return newValue;
-          }
-          return v;
-        });
+        if (user.getDebugValues().remove(this)) {
+          user.addDebugValue(newValue);
+        }
       }
       for (Phi user : debugPhiUsers()) {
-        user.getDebugValues().replaceAll(v -> {
-          if (v == this) {
-            newValue.addDebugPhiUser(user);
-            return newValue;
-          }
-          return v;
-        });
+        if (user.getDebugValues().remove(this)) {
+          user.addDebugValue(newValue);
+        }
       }
     }
     clearUsers();
   }
 
+  public void replaceInUsers(Value newValue) {
+    if (!uniqueUsers().isEmpty()) {
+      List<Instruction> toRemove = new ArrayList<>(uniqueUsers().size());
+      for (Instruction user : uniqueUsers()) {
+        user.replaceValue(this, newValue, toRemove);
+      }
+      toRemove.forEach(this::removeUser);
+    }
+    if (!uniquePhiUsers().isEmpty()) {
+      List<Phi> toRemove = new ArrayList<>(uniquePhiUsers().size());
+      for (Phi user : uniquePhiUsers()) {
+        user.replaceTrivialPhi(this, newValue, toRemove);
+      }
+      toRemove.forEach(this::removePhiUser);
+    }
+    if (debugData != null) {
+      if (!debugUsers().isEmpty()) {
+        List<Instruction> toRemove = new ArrayList<>(debugUsers().size());
+        for (Instruction user : debugUsers()) {
+          user.replaceDebugValue(this, newValue, toRemove);
+        }
+        toRemove.forEach(this::removeDebugUser);
+      }
+      if (!debugPhiUsers().isEmpty()) {
+        List<Phi> toRemove = new ArrayList<>(debugPhiUsers().size());
+        for (Phi user : debugPhiUsers()) {
+          user.replaceTrivialDebugPhi(this, newValue, toRemove);
+        }
+        toRemove.forEach(this::removeDebugPhiUser);
+      }
+    }
+  }
+
   public void setLiveIntervals(LiveIntervals intervals) {
     assert liveIntervals == null;
     liveIntervals = intervals;
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 3a89759..e78595d 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
@@ -1103,13 +1103,15 @@
             assert instruction.outValue().numberOfUsers() != 0;
             ConstNumber constNumber = instruction.asConstNumber();
             Value constantValue = instruction.outValue();
+            List<Instruction> toRemove = new ArrayList<>(constantValue.uniqueUsers().size());
             for (Instruction user : constantValue.uniqueUsers()) {
               ConstNumber newCstNum = ConstNumber.copyOf(code, constNumber);
               InstructionListIterator iterator = user.getBlock().listIterator(user);
               iterator.previous();
               iterator.add(newCstNum);
-              user.replaceValue(constantValue, newCstNum.outValue());
+              user.replaceValue(constantValue, newCstNum.outValue(), toRemove);
             }
+            toRemove.forEach(constantValue::removeUser);
           }
         }
       } else {