Merge "Do not reuse cached receiver if the original interface receiver has a local."
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index b6bc6c3..b045d44 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -69,6 +69,7 @@
           NonNull nonNull = current.asNonNull();
           Instruction origin = nonNull.origin();
           if (origin.isInvokeInterface()
+              && !origin.asInvokeInterface().getReceiver().hasLocalInfo()
               && devirtualizedCall.containsKey(origin.asInvokeInterface())
               && origin.asInvokeInterface().getReceiver() == nonNull.getAliasForOutValue()) {
             InvokeVirtual devirtualizedInvoke = devirtualizedCall.get(origin.asInvokeInterface());
@@ -95,6 +96,7 @@
               if (newReceiverType.lessThanOrEqual(oldReceiverType, appView.appInfo())
                   && dominatorTree.dominatedBy(block, devirtualizedInvoke.getBlock())) {
                 assert nonNull.src() == oldReceiver;
+                assert !oldReceiver.hasLocalInfo();
                 oldReceiver.replaceSelectiveUsers(
                     newReceiver, ImmutableSet.of(nonNull), ImmutableMap.of());
               }
@@ -171,7 +173,6 @@
               newReceiver = code.createValue(castTypeLattice);
               // Cache the new receiver with a narrower type to avoid redundant checkcast.
               if (!receiver.hasLocalInfo()) {
-                // TODO(b/118125038): Add a test for this.
                 castedReceiverCache.putIfAbsent(receiver, new IdentityHashMap<>());
                 castedReceiverCache.get(receiver).put(holderType, newReceiver);
               }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTest.java
new file mode 100644
index 0000000..80a6d8d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTest.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.devirtualize;
+
+import com.android.tools.r8.NeverMerge;
+import java.util.ArrayList;
+import java.util.List;
+
+interface TestInterface {
+  void foo();
+}
+
+@NeverMerge
+class OneUniqueImplementer implements TestInterface {
+  String boo;
+
+  OneUniqueImplementer(String boo) {
+    this.boo = boo;
+  }
+
+  @Override
+  public void foo() {
+    System.out.println("boo?! " + boo);
+  }
+}
+
+class InterfaceRenewalInLoopDebugTest {
+
+  static void booRunner(String[] boos) {
+    List<TestInterface> l = new ArrayList<>();
+    for (int i = 0; i < boos.length; i++) {
+      l.add(new OneUniqueImplementer(boos[i]));
+    }
+    TestInterface local = new OneUniqueImplementer("Initial");
+    for (int i = 0; i < l.size(); i++) {
+      local.foo();
+
+      local = l.get(i);
+      local.foo();
+
+      if (i > 0) {
+        local = l.get(i-1);
+      }
+    }
+  }
+
+  public static void main(String[] args) {
+    String[] boos = {"a", "b", "c"};
+    booRunner(boos);
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTestRunner.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTestRunner.java
new file mode 100644
index 0000000..292a83b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InterfaceRenewalInLoopDebugTestRunner.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.devirtualize;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import org.junit.Test;
+
+public class InterfaceRenewalInLoopDebugTestRunner extends DebugTestBase {
+  private static Class<?> MAIN = InterfaceRenewalInLoopDebugTest.class;
+  private static Class<?> IMPL = OneUniqueImplementer.class;
+
+  @Test
+  public void test() throws Throwable {
+    R8TestCompileResult result = testForR8(Backend.CF)
+        .setMode(CompilationMode.DEBUG)
+        .addProgramClasses(TestInterface.class, IMPL, MAIN)
+        .addKeepMainRule(MAIN)
+        .addKeepRules(ImmutableList.of("-keepattributes SourceFile,LineNumberTable"))
+        .noMinification()
+        .compile();
+
+    CodeInspector inspector = result.inspector();
+    ClassSubject mainSubject = inspector.clazz(MAIN);
+    assertThat(mainSubject, isPresent());
+    MethodSubject methodSubject = mainSubject.uniqueMethodWithName("booRunner");
+    assertThat(methodSubject, isPresent());
+    verifyDevirtualized(methodSubject);
+
+    DebugTestConfig config = result.debugConfig();
+    runDebugTest(config, MAIN.getCanonicalName(),
+        breakpoint(MAIN.getCanonicalName(), "booRunner", 37),
+        run(),
+        // At line 37
+        checkLocal("i"),
+        checkLocal("local"),
+        breakpoint(MAIN.getCanonicalName(), "booRunner", 40),
+        run(),
+        // At line 40
+        checkLocal("i"),
+        checkLocal("local"),
+        // Visiting line 37 and 40 again
+        run(),
+        checkLocal("i"),
+        checkLocal("local"),
+        run(),
+        checkLocal("i"),
+        checkLocal("local"),
+        // One more time, as boos = {"a", "b", "c"}
+        run(),
+        checkLocal("i"),
+        checkLocal("local"),
+        run(),
+        checkLocal("i"),
+        checkLocal("local"),
+        // Now run until the program finishes.
+        run());
+  }
+
+  private void verifyDevirtualized(MethodSubject method) {
+    long virtualCallCount = Streams.stream(method.iterateInstructions(instructionSubject -> {
+      if (instructionSubject.isInvokeVirtual()) {
+        return isDevirtualizedCall(instructionSubject.getMethod());
+      }
+      return false;
+    })).count();
+    long checkCastCount = Streams.stream(method.iterateInstructions(instructionSubject -> {
+      return instructionSubject.isCheckCast(IMPL.getTypeName());
+    })).count();
+    assertTrue(virtualCallCount > 0);
+    // Make sure that all devirtualized calls don't share check-casted receiver.
+    // Rather, it should use its own check-casted receiver to not pass the local.
+    assertEquals(virtualCallCount, checkCastCount);
+  }
+
+  private static boolean isDevirtualizedCall(DexMethod method) {
+    return method.getHolder().toSourceString().equals(IMPL.getTypeName())
+        && method.getArity() == 0
+        && method.proto.returnType.isVoidType()
+        && method.name.toString().equals("foo");
+  }
+
+}