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");
+ }
+
+}