Support compatibility mode for @JvmDefault

This commit is contained in:
Mikhael Bogdanov
2018-05-28 16:16:23 +02:00
parent 9b718e83a9
commit 340920fe38
19 changed files with 435 additions and 15 deletions
@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.codegen.coroutines.CoroutineCodegenUtilKt;
import org.jetbrains.kotlin.codegen.coroutines.SuspendFunctionGenerationStrategy;
import org.jetbrains.kotlin.codegen.state.GenerationState;
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper;
import org.jetbrains.kotlin.config.JvmDefaultMode;
import org.jetbrains.kotlin.config.JvmTarget;
import org.jetbrains.kotlin.config.LanguageFeature;
import org.jetbrains.kotlin.descriptors.*;
@@ -179,7 +180,10 @@ public class FunctionCodegen {
) {
OwnerKind contextKind = methodContext.getContextKind();
DeclarationDescriptor containingDeclaration = functionDescriptor.getContainingDeclaration();
if (isInterface(containingDeclaration) && !processInterfaceMethod(functionDescriptor, contextKind, false)) return;
if (isInterface(containingDeclaration) &&
!processInterfaceMethod(functionDescriptor, contextKind, false, state.getJvmDefaultMode())) {
return;
}
boolean hasSpecialBridge = hasSpecialBridgeMethod(functionDescriptor);
JvmMethodGenericSignature jvmSignature = strategy.mapMethodSignature(functionDescriptor, typeMapper, contextKind, hasSpecialBridge);
@@ -392,7 +396,7 @@ public class FunctionCodegen {
}
if (!functionDescriptor.isExternal()) {
generateMethodBody(mv, functionDescriptor, methodContext, jvmSignature, strategy, memberCodegen);
generateMethodBody(mv, functionDescriptor, methodContext, jvmSignature, strategy, memberCodegen, state.getJvmDefaultMode());
}
else if (staticInCompanionObject) {
// native @JvmStatic foo() in companion object should delegate to the static native function moved to the outer class
@@ -402,7 +406,7 @@ public class FunctionCodegen {
Method accessorMethod =
typeMapper.mapAsmMethod(memberCodegen.getContext().accessibleDescriptor(staticFunctionDescriptor, null));
Type owningType = typeMapper.mapClass((ClassifierDescriptor) staticFunctionDescriptor.getContainingDeclaration());
generateDelegateToStaticMethodBody(false, mv, accessorMethod, owningType.getInternalName());
generateDelegateToStaticMethodBody(false, mv, accessorMethod, owningType.getInternalName(), false);
}
endVisit(mv, null, origin.getElement());
@@ -574,7 +578,8 @@ public class FunctionCodegen {
@NotNull MethodContext context,
@NotNull JvmMethodSignature signature,
@NotNull FunctionGenerationStrategy strategy,
@NotNull MemberCodegen<?> parentCodegen
@NotNull MemberCodegen<?> parentCodegen,
@NotNull JvmDefaultMode jvmDefaultMode
) {
mv.visitCode();
@@ -596,6 +601,20 @@ public class FunctionCodegen {
generateFacadeDelegateMethodBody(mv, signature.getAsmMethod(), (MultifileClassFacadeContext) context.getParentContext());
methodEnd = new Label();
}
else if (isCompatibilityStubInDefaultImpls(functionDescriptor, context, jvmDefaultMode)) {
FunctionDescriptor compatibility = ((DefaultImplsClassContext) context.getParentContext()).getInterfaceContext()
.getAccessorForJvmDefaultCompatibility(functionDescriptor);
int flags = AsmUtil.getMethodAsmFlags(functionDescriptor, OwnerKind.DEFAULT_IMPLS, context.getState());
assert (flags & Opcodes.ACC_ABSTRACT) == 0 : "Interface method with body should be non-abstract" + functionDescriptor;
CallableMethod method = typeMapper.mapToCallableMethod(compatibility, false);
generateDelegateToStaticMethodBody(
true, mv,
method.getAsmMethod(),
method.getOwner().getInternalName(),
true);
methodEnd = new Label();
}
else {
FrameMap frameMap = createFrameMap(
parentCodegen.state, signature, functionDescriptor.getExtensionReceiverParameter(),
@@ -690,6 +709,16 @@ public class FunctionCodegen {
}
}
private static boolean isCompatibilityStubInDefaultImpls(
@NotNull FunctionDescriptor functionDescriptor,
@NotNull MethodContext context,
@NotNull JvmDefaultMode jvmDefaultMode
) {
return OwnerKind.DEFAULT_IMPLS == context.getContextKind() &&
hasJvmDefaultAnnotation(functionDescriptor) &&
jvmDefaultMode.isCompatibility();
}
private static void generateLocalVariableTable(
@NotNull MethodVisitor mv,
@NotNull JvmMethodSignature jvmMethodSignature,
@@ -869,7 +898,7 @@ public class FunctionCodegen {
@NotNull Method asmMethod,
@NotNull MultifileClassFacadeContext context
) {
generateDelegateToStaticMethodBody(true, mv, asmMethod, context.getFilePartType().getInternalName());
generateDelegateToStaticMethodBody(true, mv, asmMethod, context.getFilePartType().getInternalName(), false);
}
private static void generateDelegateToMethodBody(
@@ -937,9 +966,10 @@ public class FunctionCodegen {
boolean isStatic,
@NotNull MethodVisitor mv,
@NotNull Method asmMethod,
@NotNull String classToDelegateTo
@NotNull String classToDelegateTo,
boolean isInterfaceMethodCall
) {
generateDelegateToMethodBody(isStatic ? 0 : 1, mv, asmMethod, classToDelegateTo, Opcodes.INVOKESTATIC, false);
generateDelegateToMethodBody(isStatic ? 0 : 1, mv, asmMethod, classToDelegateTo, Opcodes.INVOKESTATIC, isInterfaceMethodCall);
}
private static boolean needIndexForVar(JvmMethodParameterKind kind) {
@@ -1119,7 +1149,7 @@ public class FunctionCodegen {
DeclarationDescriptor contextClass = owner.getContextDescriptor().getContainingDeclaration();
if (isInterface(contextClass) &&
!processInterfaceMethod(functionDescriptor, kind, true)) {
!processInterfaceMethod(functionDescriptor, kind, true, state.getJvmDefaultMode())) {
return;
}
@@ -1164,6 +1194,17 @@ public class FunctionCodegen {
generateFacadeDelegateMethodBody(mv, defaultMethod, (MultifileClassFacadeContext) this.owner);
endVisit(mv, "default method delegation", getSourceFromDescriptor(functionDescriptor));
}
else if (isCompatibilityStubInDefaultImpls(functionDescriptor, owner, state.getJvmDefaultMode())) {
mv.visitCode();
Method interfaceDefaultMethod = typeMapper.mapDefaultMethod(functionDescriptor, OwnerKind.IMPLEMENTATION);
generateDelegateToStaticMethodBody(
true, mv,
interfaceDefaultMethod,
typeMapper.mapOwner(functionDescriptor).getInternalName(),
true
);
endVisit(mv, "default method delegation to interface one", getSourceFromDescriptor(functionDescriptor));
}
else {
mv.visitCode();
generateDefaultImplBody(owner, functionDescriptor, mv, loadStrategy, function, memberCodegen, defaultMethod);
@@ -1538,14 +1579,15 @@ public class FunctionCodegen {
public static boolean processInterfaceMethod(
@NotNull CallableMemberDescriptor memberDescriptor,
@NotNull OwnerKind kind,
boolean isDefaultOrSynthetic
boolean isDefaultOrSynthetic,
JvmDefaultMode mode
) {
DeclarationDescriptor containingDeclaration = memberDescriptor.getContainingDeclaration();
assert isInterface(containingDeclaration) : "'processInterfaceMethod' method should be called only for interfaces, but: " +
containingDeclaration;
if (hasJvmDefaultAnnotation(memberDescriptor)) {
return kind != OwnerKind.DEFAULT_IMPLS;
return kind != OwnerKind.DEFAULT_IMPLS || mode.isCompatibility();
} else {
switch (kind) {
case DEFAULT_IMPLS: return true;
@@ -726,8 +726,19 @@ public abstract class MemberCodegen<T extends KtPureElement/* TODO: & KtDeclarat
@Override
public void doGenerateBody(@NotNull ExpressionCodegen codegen, @NotNull JvmMethodSignature signature) {
markLineNumberForElement(element.getPsiOrParent(), codegen.v);
generateMethodCallTo(original, accessor, codegen.v).coerceTo(signature.getReturnType(), null, codegen.v);
if (accessor.getName().asString().endsWith("$" + FieldAccessorKind.JVM_DEFAULT_COMPATIBILITY.getSuffix())) {
//TODO pass kind
FunctionDescriptor descriptor = unwrapFakeOverrideToAnyDeclaration(original).getOriginal();
if (descriptor != original) {
descriptor = descriptor
.copy(original.getContainingDeclaration(), descriptor.getModality(), descriptor.getVisibility(),
descriptor.getKind(), false);
}
generateMethodCallTo(descriptor, accessor, codegen.v).coerceTo(signature.getReturnType(), null, codegen.v);
}
else {
generateMethodCallTo(original, accessor, codegen.v).coerceTo(signature.getReturnType(), null, codegen.v);
}
codegen.v.areturn(signature.getReturnType());
}
@@ -52,6 +52,7 @@ import java.util.List;
import static org.jetbrains.kotlin.codegen.AsmUtil.getDeprecatedAccessFlag;
import static org.jetbrains.kotlin.codegen.AsmUtil.getVisibilityForBackingField;
import static org.jetbrains.kotlin.codegen.FunctionCodegen.processInterfaceMethod;
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isConstOrHasJvmFieldAnnotation;
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isJvmInterface;
import static org.jetbrains.kotlin.codegen.binding.CodegenBinding.DELEGATED_PROPERTIES;
@@ -352,7 +353,7 @@ public class PropertyCodegen {
if (annotations.getAllAnnotations().isEmpty()) return;
DeclarationDescriptor contextDescriptor = context.getContextDescriptor();
if (!isInterface(contextDescriptor) || FunctionCodegen.processInterfaceMethod(descriptor, kind, true)) {
if (!isInterface(contextDescriptor) || processInterfaceMethod(descriptor, kind, true, state.getJvmDefaultMode())) {
memberCodegen.generateSyntheticAnnotationsMethod(
descriptor, getSyntheticMethodSignature(descriptor), annotations, AnnotationUseSiteTarget.PROPERTY
);
@@ -405,6 +405,12 @@ public abstract class CodegenContext<T extends DeclarationDescriptor> {
return getAccessor(propertyDescriptor, FieldAccessorKind.NORMAL, null, superCallTarget, getterAccessorRequired, setterAccessorRequired);
}
public <D extends CallableMemberDescriptor> D getAccessorForJvmDefaultCompatibility(@NotNull D descriptor) {
return getAccessor(descriptor, FieldAccessorKind.JVM_DEFAULT_COMPATIBILITY, null,
(ClassDescriptor) descriptor.getContainingDeclaration());
}
@NotNull
private <D extends CallableMemberDescriptor> D getAccessor(@NotNull D descriptor, @Nullable ClassDescriptor superCallTarget) {
return getAccessor(descriptor, FieldAccessorKind.NORMAL, null, superCallTarget);
@@ -192,7 +192,7 @@ class PsiSourceCompilerForInline(private val codegen: ExpressionCodegen, overrid
else -> FunctionGenerationStrategy.FunctionDefault(state, expression as KtDeclarationWithBody)
}
FunctionCodegen.generateMethodBody(adapter, descriptor, context, jvmMethodSignature, strategy, parentCodegen)
FunctionCodegen.generateMethodBody(adapter, descriptor, context, jvmMethodSignature, strategy, parentCodegen, state.jvmDefaultMode)
if (isLambda) {
codegen.propagateChildReifiedTypeParametersUsages(parentCodegen.reifiedTypeParametersUsages)
@@ -227,6 +227,8 @@ class GenerationState private constructor(
JVMConstructorCallNormalizationMode.DEFAULT
)
val jvmDefaultMode = languageVersionSettings.getFlag(AnalysisFlag.jvmDefaultMode)
init {
val disableOptimization = configuration.get(JVMConfigurationKeys.DISABLE_OPTIMIZATION, false)
@@ -24,6 +24,7 @@ enum class FieldAccessorKind(val suffix: String) {
IN_CLASS_COMPANION("cp"),
FIELD_FROM_LOCAL("lp"),
LATEINIT_INTRINSIC("li"),
JVM_DEFAULT_COMPATIBILITY("jd")
}
private fun CallableMemberDescriptor.getJvmName() =
@@ -42,6 +43,6 @@ fun getAccessorNameSuffix(
else ->
throw UnsupportedOperationException("Do not know how to create accessor for descriptor " + descriptor)
}
if (accessorKind == FieldAccessorKind.JVM_DEFAULT_COMPATIBILITY) return suffix + "$" + FieldAccessorKind.JVM_DEFAULT_COMPATIBILITY.suffix
return if (superCallDescriptor == null) suffix else "$suffix\$s${superCallDescriptor.name.asString().hashCode()}"
}
@@ -0,0 +1,38 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// FILE: Simple.java
public interface Simple extends KInterface2 {
default String test() {
return KInterface2.DefaultImpls.test2(this, "OK");
}
}
// FILE: Foo.java
public class Foo implements Simple {
}
// FILE: main.kt
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface KInterface<T> {
@JvmDefault
fun test2(p: T): T {
return p
}
}
interface KInterface2 : KInterface<String> {
}
fun box(): String {
val result = Foo().test()
if (result != "OK") return "fail 1: ${result}"
return Foo().test2("OK")
}
@@ -0,0 +1,36 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// FILE: Simple.java
public interface Simple extends KInterface2 {
default String test() {
return KInterface2.DefaultImpls.test2(this, "OK");
}
}
// FILE: Foo.java
public class Foo implements Simple {
public String test2(String p) {
return "fail";
}
}
// FILE: main.kt
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface KInterface<T> {
@JvmDefault
fun test2(p: T): T {
return p
}
}
interface KInterface2 : KInterface<String> {
}
fun box(): String {
return Foo().test()
}
@@ -0,0 +1,40 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// FILE: Simple.java
public interface Simple extends KInterface3 {
default String test() {
return KInterface3.DefaultImpls.test2(this, "OK");
}
}
// FILE: Foo.java
public class Foo implements Simple {
public String test2(String p) {
return "fail";
}
}
// FILE: main.kt
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface KInterface<T> {
@JvmDefault
fun test2(p: T): T {
return p
}
}
interface KInterface2 : KInterface<String> {
}
interface KInterface3 : KInterface2 {
}
fun box(): String {
return Foo().test()
}
@@ -0,0 +1,22 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// JVM_TARGET: 1.8
// WITH_RUNTIME
// FULL_JDK
interface Test {
@JvmDefault
fun test(s: String ="OK"): String {
return s
}
}
class TestClass : Test {
}
fun box(): String {
val defaultImpls = java.lang.Class.forName(Test::class.java.canonicalName + "\$DefaultImpls")
val declaredMethod = defaultImpls.getDeclaredMethod("test\$default", Test::class.java, String::class.java, Int::class.java, Any::class.java)
return declaredMethod.invoke(null, TestClass(), null, 1, null) as String
}
@@ -0,0 +1,37 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// FILE: Simple.java
public interface Simple extends KInterface2 {
default String test() {
return KInterface2.DefaultImpls.test2(this);
}
}
// FILE: Foo.java
public class Foo implements Simple {
}
// FILE: main.kt
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface KInterface {
@JvmDefault
fun test2(): String {
return "OK"
}
}
interface KInterface2 : KInterface {
}
fun box(): String {
val result = Foo().test()
if (result != "OK") return "fail 1: ${result}"
return Foo().test2()
}
@@ -0,0 +1,33 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// FILE: Simple.java
public interface Simple extends KInterface {
default String test() {
return KInterface.DefaultImpls.test2(this);
}
}
// FILE: Foo.java
public class Foo implements Simple {
}
// FILE: main.kt
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface KInterface {
@JvmDefault
fun test2(): String {
return "OK"
}
}
fun box(): String {
val result = Foo().test()
if (result != "OK") return "fail 1: ${result}"
return Foo().test2()
}
@@ -0,0 +1,20 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// JVM_TARGET: 1.8
// WITH_RUNTIME
// FULL_JDK
interface KInterface {
@JvmDefault
fun test(s: String ="OK"): String {
return s
}
}
// 1 INVOKESTATIC KInterface.access\$test\$jd
// 1 INVOKESTATIC KInterface.test\$default
// from $default
// 1 INVOKEINTERFACE KInterface.test
//from $jd
// 1 INVOKESPECIAL KInterface.test
@@ -0,0 +1,20 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// JVM_TARGET: 1.8
interface KInterface {
@JvmDefault
fun test2(): String {
return "OK"
}
}
interface KInterface2 : KInterface {
}
// 1 INVOKESTATIC KInterface2.access\$test2\$jd
// 1 INVOKESTATIC KInterface.access\$test2\$jd
// 1 INVOKESPECIAL KInterface2.test2
// 1 INVOKESPECIAL KInterface.test2
@@ -0,0 +1,24 @@
// !API_VERSION: 1.3
// !JVM_DEFAULT_MODE: compatibility
// JVM_TARGET: 1.8
interface KInterface {
@JvmDefault
fun test2(): String {
return "OK"
}
}
interface KInterface2 : KInterface {
@JvmDefault
abstract override fun test2(): String
}
// 1 INVOKESTATIC KInterface.access\$test2\$jd
// +
// 0 INVOKESTATIC KInterface2.access\$test2\$jd
// =
// 1 INVOKESTATIC
// 1 INVOKESPECIAL KInterface.test2
// 0 INVOKESPECIAL KInterface2.test2
@@ -481,6 +481,49 @@ public class BlackBoxWithJava8CodegenTestGenerated extends AbstractBlackBoxCodeg
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/superCall.kt");
}
@TestMetadata("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Compatibility extends AbstractBlackBoxCodegenTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInCompatibility() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("bridge.kt")
public void testBridge() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/bridge.kt");
}
@TestMetadata("bridge2.kt")
public void testBridge2() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/bridge2.kt");
}
@TestMetadata("bridge3.kt")
public void testBridge3() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/bridge3.kt");
}
@TestMetadata("defaultArgs.kt")
public void testDefaultArgs() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/defaultArgs.kt");
}
@TestMetadata("inheritedJvmDefault.kt")
public void testInheritedJvmDefault() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/inheritedJvmDefault.kt");
}
@TestMetadata("simpleFunction.kt")
public void testSimpleFunction() throws Exception {
runTest("compiler/testData/codegen/java8/box/jvm8/defaults/compatibility/simpleFunction.kt");
}
}
@TestMetadata("compiler/testData/codegen/java8/box/jvm8/defaults/delegationBy")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -51,4 +51,45 @@ public class BytecodeTextJava8TestGenerated extends AbstractBytecodeTextTest {
runTest("compiler/testData/codegen/java8/bytecodeText/hashCode/hashCode.kt");
}
}
@TestMetadata("compiler/testData/codegen/java8/bytecodeText/jvmDefault")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class JvmDefault extends AbstractBytecodeTextTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInJvmDefault() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/java8/bytecodeText/jvmDefault"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("compiler/testData/codegen/java8/bytecodeText/jvmDefault/compatibility")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class Compatibility extends AbstractBytecodeTextTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInCompatibility() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/java8/bytecodeText/jvmDefault/compatibility"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("defaultArgs.kt")
public void testDefaultArgs() throws Exception {
runTest("compiler/testData/codegen/java8/bytecodeText/jvmDefault/compatibility/defaultArgs.kt");
}
@TestMetadata("simpleFunction.kt")
public void testSimpleFunction() throws Exception {
runTest("compiler/testData/codegen/java8/bytecodeText/jvmDefault/compatibility/simpleFunction.kt");
}
@TestMetadata("simpleFunctionWithAbstractOverride.kt")
public void testSimpleFunctionWithAbstractOverride() throws Exception {
runTest("compiler/testData/codegen/java8/bytecodeText/jvmDefault/compatibility/simpleFunctionWithAbstractOverride.kt");
}
}
}
}
@@ -13,6 +13,9 @@ enum class JvmDefaultMode(val description: String) {
val isEnabled
get() = this != DISABLE
val isCompatibility
get() = this == ENABLE_WITH_DEFAULT_IMPLS
companion object {
@JvmField
val DEFAULT = DISABLE