blob: d08bb2fb85b07ddb02b0621e2d827a5658f45684 [file] [log] [blame]
// Copyright (c) 2018, 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.bridgeremoval;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import com.android.tools.r8.KeepConstantArguments;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.dex.code.DexInvokeVirtual;
import com.android.tools.r8.dex.code.DexReturnVoid;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.jasmin.JasminBuilder;
import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class B77836766 extends TestBase {
private final TestParameters parameters;
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
return getTestParameters().withAllRuntimesAndApiLevels().build();
}
public B77836766(TestParameters parameters) {
this.parameters = parameters;
}
/**
* The below Jasmin code mimics the following Kotlin code:
*
* package java_pkg;
*
* public interface Itf1 {
* void foo(String arg);
* }
*
* public interface Itf2 {
* void foo(Integer arg);
* }
*
* package kt_pkg;
*
* internal abstract class AbsCls<T> {
* void foo(T obj) { ... }
* }
*
* internal class Cls1() : AbsCls<String>(), Itf1
*
* internal class Cls2() : AbsCls<Integer>(), Itf2
*
*
* kotlinc introduced bridge methods Cls?#foo to AbsCls#foo:
*
* class Cls1 extends AbsCls implements Itf1 {
* public bridge synthetic void foo(String arg) {
* invoke-virtual Cls1#foo(Object)V
* }
* }
*
* Note that we can't write such code in Java because javac requires Itf?#foo, which are
* technically abstract methods, to be explicitly overridden.
*/
@Test
public void test_bridgeTargetInBase_differentBridges() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder absCls = jasminBuilder.addClass("AbsCls");
absCls.setAccess("public abstract");
absCls.addFinalMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
ClassBuilder itf1 = jasminBuilder.addInterface("Itf1");
itf1.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V");
ClassBuilder cls1 = jasminBuilder.addClass("Cls1", absCls.name, itf1.name);
// Mimic Kotlin's "internal" class
cls1.setAccess("");
cls1.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder itf2 = jasminBuilder.addInterface("Itf2");
itf2.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
ClassBuilder cls2Class = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
// Mimic Kotlin's "internal" class
cls2Class.setAccess("");
cls2Class.addBridgeMethod(
"foo",
ImmutableList.of("Ljava/lang/Integer;"),
"V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + cls1.name,
"dup",
"invokespecial " + cls1.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Hello\"",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/String;)V",
"new " + cls2Class.name,
"dup",
"invokespecial " + cls2Class.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Integer;)V",
"return");
testForR8(parameters.getBackend())
.addProgramClassFileData(jasminBuilder.buildClasses())
.addKeepMainRule(mainClass.name)
.addOptionsModification(this::configure)
.enableConstantArgumentAnnotations()
.noHorizontalClassMerging(cls2Class.name)
.addDontObfuscate()
.setMinApi(parameters.getApiLevel())
.compile()
.inspect(
inspector -> {
ClassSubject absSubject = inspector.clazz(absCls.name);
assertThat(absSubject, isPresent());
ClassSubject cls1Subject = inspector.clazz(cls1.name);
assertThat(cls1Subject, isPresent());
ClassSubject cls2Subject = inspector.clazz(cls2Class.name);
assertThat(cls2Subject, isPresent());
// Cls1#foo and Cls2#foo should not refer to each other.
// They can invoke their own bridge method or AbsCls#foo (via member rebinding).
// Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
assertThat(fooInCls2, not(isPresent()));
MethodSubject fooFromCls2InAbsCls =
absSubject.method("void", "foo", "java.lang.Integer");
assertThat(fooFromCls2InAbsCls, isPresent());
// Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
assertThat(fooInCls1, not(isPresent()));
MethodSubject fooFromCls1InAbsCls =
absSubject.method("void", "foo", "java.lang.String");
assertThat(fooFromCls1InAbsCls, isPresent());
if (parameters.isDexRuntime()) {
DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
DexInvokeVirtual invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
}
})
.run(parameters.getRuntime(), mainClass.name)
.assertSuccessWithOutput("Hello0");
}
/**
* class Base {
* void foo(Object o) {...}
* }
* interface ItfInteger {
* void foo(Integer o);
* }
* class DerivedInteger extends Base implements ItfInteger {
* // Bridge method deferring to Base#foo(Object):
* public bridge synthetic void foo(Integer o) {
* foo((Object) o);
* } }
* class DerivedString extends Base {
* // Regular non-bridge method calling Base#foo(Object):
* public void bar(String o) {
* foo(o);
* } }
*/
@Test
public void test_bridgeTargetInBase_bridgeAndNonBridge() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder baseCls = jasminBuilder.addClass("Base");
baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
ClassBuilder itf = jasminBuilder.addInterface("ItfInteger");
itf.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
ClassBuilder derivedIntegerClass =
jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
derivedIntegerClass.addBridgeMethod(
"foo",
ImmutableList.of("Ljava/lang/Integer;"),
"V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
cls2.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
cls2.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + derivedIntegerClass.name,
"dup",
"invokespecial " + derivedIntegerClass.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Integer;)V",
"new " + cls2.name,
"dup",
"invokespecial " + cls2.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + cls2.name + "/bar(Ljava/lang/String;)V",
"return");
testForR8(parameters.getBackend())
.addProgramClassFileData(jasminBuilder.buildClasses())
.addKeepMainRule(mainClass.name)
.addOptionsModification(this::configure)
.enableConstantArgumentAnnotations()
.noHorizontalClassMerging(derivedIntegerClass.name)
.addDontObfuscate()
.setMinApi(parameters.getApiLevel())
.compile()
.inspect(
inspector -> {
ClassSubject baseSubject = inspector.clazz(baseCls.name);
assertThat(baseSubject, isPresent());
ClassSubject cls1Subject = inspector.clazz(derivedIntegerClass.name);
assertThat(cls1Subject, isPresent());
ClassSubject cls2Subject = inspector.clazz(cls2.name);
assertThat(cls2Subject, isPresent());
// Cls1#foo and Cls2#bar should refer to Base#foo.
MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
assertThat(barInCls2, isPresent());
// Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
assertThat(fooInCls1, not(isPresent()));
MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
assertThat(fooInBase, isPresent());
if (parameters.isDexRuntime()) {
DexCode code = barInCls2.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
DexInvokeVirtual invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
code = fooInBase.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
}
})
.run(parameters.getRuntime(), mainClass.name)
.assertSuccessWithOutput("0Bar");
}
/**
* class Base {
* protected void foo(Object o) { ... }
* // Bridge method deferring to Base#foo(Object):
* public bridge synthetic void foo(Integer o) {
* foo((Object) o);
* } }
* class DerivedString extends Base {
* // Regular non-bridge method calling Base#foo(Object):
* public void bar(String o) {
* foo(o);
* } }
*/
@Test
public void test_nonBridgeInSubType() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder baseCls = jasminBuilder.addClass("Base");
baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
baseCls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + baseCls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder subCls = jasminBuilder.addClass("DerivedString", baseCls.name);
subCls.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
subCls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + subCls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + baseCls.name,
"dup",
"invokespecial " + baseCls.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + baseCls.name + "/foo(Ljava/lang/Integer;)V",
"new " + subCls.name,
"dup",
"invokespecial " + subCls.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + subCls.name + "/bar(Ljava/lang/String;)V",
"return"
);
testForR8(parameters.getBackend())
.addProgramClassFileData(jasminBuilder.buildClasses())
.addKeepMainRule(mainClass.name)
.addOptionsModification(this::configure)
.enableConstantArgumentAnnotations()
.addDontObfuscate()
.setMinApi(parameters.getApiLevel())
.compile()
.inspect(
inspector -> {
ClassSubject baseSubject = inspector.clazz(baseCls.name);
assertThat(baseSubject, isPresent());
ClassSubject subSubject = inspector.clazz(subCls.name);
assertThat(subSubject, isPresent());
// DerivedString2#bar should refer to Base#foo.
MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
assertThat(barInSub, isPresent());
if (parameters.isDexRuntime()) {
DexCode code = barInSub.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
DexInvokeVirtual invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
}
})
.run(parameters.getRuntime(), mainClass.name)
.assertSuccessWithOutput("0Bar");
}
/*
* public class Base {
* public bridge void foo(Integer i) { foo((Object) i); }
* public foo(Object o) { print(o); }
* public bar(String s) { foo(s); }
* }
*/
@Test
public void test_bridgeTargetInsideTheSameClass() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder cls = jasminBuilder.addClass("Base");
cls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
"return");
cls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
cls.addRuntimeInvisibleAnnotation(KeepConstantArguments.class.getTypeName());
cls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + cls.name,
"dup",
"invokespecial " + cls.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Integer;)V",
"new " + cls.name,
"dup",
"invokespecial " + cls.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + cls.name + "/bar(Ljava/lang/String;)V",
"return"
);
testForR8(parameters.getBackend())
.addProgramClassFileData(jasminBuilder.buildClasses())
.addKeepMainRule(mainClass.name)
.addOptionsModification(this::configure)
.enableConstantArgumentAnnotations()
.addDontObfuscate()
.setMinApi(parameters.getApiLevel())
.compile()
.inspect(
inspector -> {
ClassSubject baseSubject = inspector.clazz(cls.name);
assertThat(baseSubject, isPresent());
// Base#bar should remain as-is, i.e., refer to Base#foo(Object).
MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
assertThat(barInSub, isPresent());
if (parameters.isDexRuntime()) {
DexCode code = barInSub.getMethod().getCode().asDexCode();
checkInstructions(
code, ImmutableList.of(DexInvokeVirtual.class, DexReturnVoid.class));
DexInvokeVirtual invoke = (DexInvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
}
})
.run(parameters.getRuntime(), mainClass.name)
.assertSuccessWithOutput("0Bar");
}
private void configure(InternalOptions options) {
options.callSiteOptimizationOptions().setEnableMethodStaticizing(false);
// Callees are invoked with a simple constant, e.g., "Bar". Propagating it into the callees
// bothers what the tests want to check, such as exact instructions in the body that include
// invocation kinds, like virtual call to a bridge.
// Disable inlining to avoid the (short) tested method from being inlined and then removed.
options.inlinerOptions().enableInlining = false;
}
}