IR: Align Interface Defaults with Old Backend

Change the treatment of default implementations on interfaces in JVM
compatibility mode. Previously, the IR backend moved the actual
default implementation to the DefaultImpls class, and then bridged to
it from the interface default. The old backend did the reverse, at the
cost of an additional accessor, in order to gain better binary
compatibility properties. See #2612 for discussion.

The accessor needs to call a specific implementation, so must be
performed through an `invokespecial`. We trick the
SyntheticAccessorLowering into doing this for us, by marking the
bridging call as a super call. We do this in want of an explicit
`invokespecial` Ir Node.

InterfaceDefaultCallsPhase previously assumed the old behaviour of the
IR backend (that calls to default implementations, e.g. `foo$default`
should target `DefaultImpls.foo$default`). But now the bridge to
foo$default resides on `DefaultImpls` already, causing that pass to
create a recursive loop. We cut that loop with a simple check.
This commit is contained in:
Kristoffer Andersen
2019-11-05 15:37:43 +01:00
committed by max-kammerer
parent b5de625350
commit d1c2862e27
15 changed files with 187 additions and 92 deletions
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.backend.jvm.lower
import org.jetbrains.kotlin.backend.common.FileLoweringPass
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.ir.isMethodOfAny
import org.jetbrains.kotlin.backend.common.ir.passTypeArgumentsFrom
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
@@ -164,7 +165,7 @@ internal val interfaceDefaultCallsPhase = makeIrFilePhase(
description = "Redirect interface calls with default arguments to DefaultImpls"
)
private class InterfaceDefaultCallsLowering(val context: JvmBackendContext) : IrElementTransformerVoid(), FileLoweringPass {
private class InterfaceDefaultCallsLowering(val context: JvmBackendContext) : IrElementTransformerVoidWithContext(), FileLoweringPass {
// TODO If there are no default _implementations_ we can avoid generating defaultImpls class entirely by moving default arg dispatchers to the interface class
override fun lower(irFile: IrFile) {
irFile.transformChildrenVoid(this)
@@ -181,6 +182,12 @@ private class InterfaceDefaultCallsLowering(val context: JvmBackendContext) : Ir
}
val redirectTarget = context.declarationFactory.getDefaultImplsFunction(callee as IrSimpleFunction)
// InterfaceLowering bridges from DefaultImpls in compatibility mode -- if that's the case,
// this phase will inadvertently cause a recursive loop as the bridge on the DefaultImpls
// gets redirected to call itself.
if (redirectTarget == currentFunction?.irElement) return super.visitCall(expression)
val newCall = irCall(expression, redirectTarget, receiversAsArguments = true)
return super.visitCall(newCall)
@@ -9,7 +9,6 @@ import org.jetbrains.kotlin.backend.common.ClassLoweringPass
import org.jetbrains.kotlin.backend.common.ir.copyBodyToStatic
import org.jetbrains.kotlin.backend.common.ir.isMethodOfAny
import org.jetbrains.kotlin.backend.common.ir.passTypeArgumentsFrom
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
import org.jetbrains.kotlin.backend.jvm.codegen.isJvmInterface
@@ -17,10 +16,6 @@ import org.jetbrains.kotlin.backend.jvm.ir.hasJvmDefault
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.irBlockBody
import org.jetbrains.kotlin.ir.builders.irExprBody
import org.jetbrains.kotlin.ir.builders.irReturn
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.*
@@ -74,7 +69,12 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
/**
* 2) They inherit a default implementation from an interface this interface
* extends: create a bridge from companion to companion, if necessary:
* extends: create a bridge from companion to companion, unless
* - the implementation is private or belongs to java.lang.Object
* - we're in JVM Compatibility Default mode, in which case we go via
* accessors on the parent class rather than the DefaultImpls
* - we're in JVM Default mode, and we have that default implementation,
* in which case we simply leave it.
*
* ```
* interface A { fun foo() = 0 }
@@ -91,11 +91,20 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
function.origin == IrDeclarationOrigin.FAKE_OVERRIDE -> {
val implementation = function.resolveFakeOverride()!!
if (!Visibilities.isPrivate(implementation.visibility)
&& !implementation.isMethodOfAny()
&& (!implementation.hasJvmDefault() || context.state.jvmDefaultMode.isCompatibility)
) {
delegateInheritedDefaultImplementationToDefaultImpls(function, implementation)
when {
Visibilities.isPrivate(implementation.visibility) || implementation.isMethodOfAny() ->
continue@loop
context.state.jvmDefaultMode.isCompatibility -> {
val defaultImpl = createDefaultImpl(function)
defaultImpl.bridgeViaAccessorTo(function)
}
!implementation.hasJvmDefault() -> {
val defaultImpl = createDefaultImpl(function)
context.declarationFactory.getDefaultImplsFunction(implementation).also {
defaultImpl.bridgeToStatic(it)
}
}
// else -> Do nothing.
}
}
@@ -106,7 +115,9 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
Visibilities.isPrivate(function.visibility)
|| (function.origin == IrDeclarationOrigin.FUNCTION_FOR_DEFAULT_PARAMETER && !function.hasJvmDefault())
|| function.origin == JvmLoweredDeclarationOrigin.SYNTHETIC_METHOD_FOR_PROPERTY_ANNOTATIONS -> {
removedFunctions[function.symbol] = createDefaultImpl(function).symbol
val defaultImpl = createDefaultImpl(function)
function.copyImplementationTo(defaultImpl)
removedFunctions[function.symbol] = defaultImpl.symbol
}
/**
@@ -114,7 +125,8 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
* an abstract stub is left.
*/
!function.hasJvmDefault() -> {
createDefaultImpl(function)
val defaultImpl = createDefaultImpl(function)
function.copyImplementationTo(defaultImpl)
function.body = null
//TODO reset modality to abstract
}
@@ -124,7 +136,7 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
*/
context.state.jvmDefaultMode.isCompatibility -> {
val defaultImpl = createDefaultImpl(function)
function.body = createDelegatingCall(defaultImpl, function)
defaultImpl.bridgeViaAccessorTo(function)
}
// 6) ... otherwise we simply leave the default function implementation on the interface.
@@ -163,40 +175,52 @@ internal class InterfaceLowering(val context: JvmBackendContext) : IrElementTran
}
private fun createDefaultImpl(function: IrSimpleFunction): IrSimpleFunction =
context.declarationFactory.getDefaultImplsFunction(function).also { newFunction ->
context.declarationFactory.getDefaultImplsFunction(function).also {newFunction ->
newFunction.body = function.body?.patchDeclarationParents(newFunction)
copyBodyToStatic(function, newFunction)
newFunction.parentAsClass.declarations.add(newFunction)
}
private fun createDelegatingCall(defaultImpls: IrFunction, interfaceMethod: IrFunction): IrExpressionBody {
val startOffset = interfaceMethod.startOffset
val endOffset = interfaceMethod.endOffset
private fun IrSimpleFunction.copyImplementationTo(target: IrSimpleFunction) {
copyBodyToStatic(this, target)
}
return IrExpressionBodyImpl(IrCallImpl(startOffset, endOffset, interfaceMethod.returnType, defaultImpls.symbol).apply {
passTypeArgumentsFrom(interfaceMethod)
var offset = 0
interfaceMethod.dispatchReceiverParameter?.let {
putValueArgument(offset++, IrGetValueImpl(startOffset, endOffset, it.symbol))
}
interfaceMethod.extensionReceiverParameter?.let {
putValueArgument(offset++, IrGetValueImpl(startOffset, endOffset, it.symbol))
}
interfaceMethod.valueParameters.forEachIndexed { i, it ->
putValueArgument(i + offset, IrGetValueImpl(startOffset, endOffset, it.symbol))
// Bridge from static to static method - simply fill the arguments to the parameters.
// By nature of the generation of both source and target of bridge, they line up.
private fun IrFunction.bridgeToStatic(callTarget: IrFunction) {
body = IrExpressionBodyImpl(IrCallImpl(startOffset, endOffset, returnType, callTarget.symbol).also { call ->
call.passTypeArgumentsFrom(this)
valueParameters.forEachIndexed { i, it ->
call.putValueArgument(i, IrGetValueImpl(startOffset, endOffset, it.symbol))
}
})
}
private fun delegateInheritedDefaultImplementationToDefaultImpls(fakeOverride: IrSimpleFunction, implementation: IrSimpleFunction) {
val defaultImplFun = context.declarationFactory.getDefaultImplsFunction(implementation)
val irFunction = context.declarationFactory.getDefaultImplsFunction(fakeOverride)
// Bridge from static DefaultImpl method to the interface method. Arguments need to
// be shifted in presence of dispatch and extension receiver.
private fun IrFunction.bridgeViaAccessorTo(callTarget: IrFunction) {
body = IrExpressionBodyImpl(
IrCallImpl(
startOffset,
endOffset,
returnType,
callTarget.symbol,
superQualifierSymbol = callTarget.parentAsClass.symbol
).also { call ->
call.passTypeArgumentsFrom(this)
irFunction.parentAsClass.declarations.add(irFunction)
context.createIrBuilder(irFunction.symbol, UNDEFINED_OFFSET, UNDEFINED_OFFSET).apply {
irFunction.body = createDelegatingCall(defaultImplFun, irFunction)
}
var offset = 0
callTarget.dispatchReceiverParameter?.let {
call.dispatchReceiver = IrGetValueImpl(startOffset, endOffset, valueParameters[offset].symbol)
offset += 1
}
callTarget.extensionReceiverParameter?.let {
call.extensionReceiver = IrGetValueImpl(startOffset, endOffset, valueParameters[offset].symbol)
offset += 1
}
for (i in offset until valueParameters.size) {
call.putValueArgument(i - 1, IrGetValueImpl(startOffset, endOffset, valueParameters[i].symbol))
}
})
}
override fun visitReturn(expression: IrReturn): IrExpression {
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.backend.common.ir.passTypeArgumentsFrom
import org.jetbrains.kotlin.backend.common.ir.remapTypeParameters
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
import org.jetbrains.kotlin.backend.jvm.codegen.isJvmInterface
import org.jetbrains.kotlin.backend.jvm.intrinsics.receiverAndArgs
import org.jetbrains.kotlin.backend.jvm.ir.IrInlineReferenceLocator
import org.jetbrains.kotlin.backend.jvm.ir.isLambda
@@ -187,48 +188,52 @@ internal class SyntheticAccessorLowering(val context: JvmBackendContext) : IrEle
private fun IrSimpleFunction.makeSimpleFunctionAccessor(expression: IrCall): IrSimpleFunction {
val source = this
// Find the right container to insert the accessor. Simply put, when we call a function on a class A,
// we also need to put its accessor into A. However, due to the way that calls are implemented in the
// IR we generally need to look at the type of the dispatchReceiver *argument* in order to find the
// correct class. Consider the following code:
//
// fun run(f : () -> Int): Int = f()
//
// open class A {
// private fun f() = 0
// fun g() = run { this.f() }
// }
//
// class B : A {
// override fun g() = 1
// fun h() = run { super.g() }
// }
//
// We have calls to the private methods A.f from a generated Lambda subclass for the argument to `run`
// in class A and a super call to A.g from a generated Lambda subclass in class B.
//
// In the first case, we need to produce an accessor in class A to access the private member of A.
// Both the parent of the function f and the type of the dispatch receiver point to the correct class.
// In the second case we need to call A.g from within class B, since this is the only way to invoke
// a method of a superclass on the JVM. However, the IR for the call to super.g points directly to the
// function g in class A. Confusingly, the `superQualifier` on this call also points to class A.
// The only way to compute the actual enclosing class for the call is by looking at the type of the
// dispatch receiver argument, which points to B.
//
// Beyond this, there can be accessors that are needed because other lowerings produce code calling
// private methods (e.g., local functions for lambdas are private and called from generated
// SAM wrapper classes). In this case we rely on the parent field of the called function.
//
// Finally, we need to produce accessors for calls to protected static methods coming from Java,
// which we put in the closest enclosing class which has access to the method in question.
val dispatchReceiverType = expression.dispatchReceiver?.type
val parent = source.accessorParent(dispatchReceiverType?.classOrNull?.owner ?: source.parent)
return buildFun {
origin = JvmLoweredDeclarationOrigin.SYNTHETIC_ACCESSOR
name = source.accessorName()
visibility = Visibilities.PUBLIC
modality = if (parent is IrClass && parent.isJvmInterface) Modality.OPEN else Modality.FINAL
isSuspend = source.isSuspend // synthetic accessors of suspend functions are handled in codegen
}.also { accessor ->
// Find the right container to insert the accessor. Simply put, when we call a function on a class A,
// we also need to put its accessor into A. However, due to the way that calls are implemented in the
// IR we generally need to look at the type of the dispatchReceiver *argument* in order to find the
// correct class. Consider the following code:
//
// fun run(f : () -> Int): Int = f()
//
// open class A {
// private fun f() = 0
// fun g() = run { this.f() }
// }
//
// class B : A {
// override fun g() = 1
// fun h() = run { super.g() }
// }
//
// We have calls to the private methods A.f from a generated Lambda subclass for the argument to `run`
// in class A and a super call to A.g from a generated Lambda subclass in class B.
//
// In the first case, we need to produce an accessor in class A to access the private member of A.
// Both the parent of the function f and the type of the dispatch receiver point to the correct class.
// In the second case we need to call A.g from within class B, since this is the only way to invoke
// a method of a superclass on the JVM. However, the IR for the call to super.g points directly to the
// function g in class A. Confusingly, the `superQualifier` on this call also points to class A.
// The only way to compute the actual enclosing class for the call is by looking at the type of the
// dispatch receiver argument, which points to B.
//
// Beyond this, there can be accessors that are needed because other lowerings produce code calling
// private methods (e.g., local functions for lambdas are private and called from generated
// SAM wrapper classes). In this case we rely on the parent field of the called function.
//
// Finally, we need to produce accessors for calls to protected static methods coming from Java,
// which we put in the closest enclosing class which has access to the method in question.
val dispatchReceiverType = expression.dispatchReceiver?.type
accessor.parent = source.accessorParent(dispatchReceiverType?.classOrNull?.owner ?: source.parent)
accessor.parent = parent
pendingTransformations.add { (accessor.parent as IrDeclarationContainer).declarations.add(accessor) }
accessor.copyTypeParametersFrom(source, JvmLoweredDeclarationOrigin.SYNTHETIC_ACCESSOR)
@@ -0,0 +1,11 @@
// !JVM_DEFAULT_MODE: compatibility
// TARGET_BACKEND: JVM
// JVM_TARGET: 1.8
// WITH_RUNTIME
interface A {
@JvmDefault
fun String.foo() = "OK"
}
fun box(): String =
object : A { fun box() = "FAIL".foo() }.box()
@@ -1,5 +1,4 @@
// !JVM_DEFAULT_MODE: compatibility
// IGNORE_BACKEND: JVM_IR
// JVM_TARGET: 1.8
// FULL_JDK
@@ -10,7 +9,7 @@ interface KInterface {
}
}
// 1 INVOKESTATIC KInterface.access\$test\$jd
// 1 INVOKESTATIC KInterface.access\$test\$
// 1 INVOKESTATIC KInterface.test\$default
// from $default
@@ -0,0 +1,22 @@
// !JVM_DEFAULT_MODE: compatibility
// JVM_TARGET: 1.8
interface A {
@JvmDefault
fun foo() = "FAIL"
}
interface Left : A { }
interface Right : A {
@JvmDefault
override fun foo() = "OK"
}
interface C : Left, Right {}
fun box(): String {
val x = object : C {}
return x.foo()
}
// 0 INVOKESTATIC .*\$DefaultImpls\.foo
@@ -1,5 +1,4 @@
// !JVM_DEFAULT_MODE: compatibility
// IGNORE_BACKEND: JVM_IR
// JVM_TARGET: 1.8
interface KInterface {
@@ -13,8 +12,8 @@ interface KInterface2 : KInterface {
}
// 1 INVOKESTATIC KInterface2.access\$test2\$jd
// 1 INVOKESTATIC KInterface.access\$test2\$jd
// 1 INVOKESTATIC KInterface2.access\$test2\$
// 1 INVOKESTATIC KInterface.access\$test2\$
// 1 INVOKESPECIAL KInterface2.test2
// 1 INVOKESPECIAL KInterface.test2
@@ -1,5 +1,4 @@
// !JVM_DEFAULT_MODE: compatibility
// IGNORE_BACKEND: JVM_IR
// JVM_TARGET: 1.8
interface KInterface {
@@ -14,11 +13,11 @@ interface KInterface2 : KInterface {
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
// 1 INVOKESTATIC KInterface.access\$test2\$
// +
// 0 INVOKESTATIC KInterface2.access\$test2\$
// =
// 1 INVOKESTATIC KInterface
@@ -1,5 +1,4 @@
// !JVM_DEFAULT_MODE: compatibility
// IGNORE_BACKEND: JVM_IR
// JVM_TARGET: 1.8
interface KInterface {
@@ -14,10 +13,10 @@ interface KInterface2 : KInterface {
}
// 1 INVOKESTATIC KInterface2.access\$getBar\$jd
// 1 INVOKESTATIC KInterface2.access\$setBar\$jd
// 1 INVOKESTATIC KInterface.access\$getBar\$jd
// 1 INVOKESTATIC KInterface.access\$setBar\$jd
// 1 INVOKESTATIC KInterface2.access\$getBar\$
// 1 INVOKESTATIC KInterface2.access\$setBar\$
// 1 INVOKESTATIC KInterface.access\$getBar\$
// 1 INVOKESTATIC KInterface.access\$setBar\$
// 1 INVOKESPECIAL KInterface2.getBar
// 1 INVOKESPECIAL KInterface2.setBar
@@ -14863,6 +14863,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/inheritedJvmDefault.kt");
}
@TestMetadata("interfaceExtension.kt")
public void testInterfaceExtension() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/interfaceExtension.kt");
}
@TestMetadata("propertyAnnotation.kt")
public void testPropertyAnnotation() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/propertyAnnotation.kt");
@@ -3002,6 +3002,11 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/defaultArgs.kt");
}
@TestMetadata("simpleDiamond.kt")
public void testSimpleDiamond() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/simpleDiamond.kt");
}
@TestMetadata("simpleFunction.kt")
public void testSimpleFunction() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/simpleFunction.kt");
@@ -14863,6 +14863,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/inheritedJvmDefault.kt");
}
@TestMetadata("interfaceExtension.kt")
public void testInterfaceExtension() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/interfaceExtension.kt");
}
@TestMetadata("propertyAnnotation.kt")
public void testPropertyAnnotation() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/propertyAnnotation.kt");
@@ -13713,6 +13713,11 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/inheritedJvmDefault.kt");
}
@TestMetadata("interfaceExtension.kt")
public void testInterfaceExtension() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/interfaceExtension.kt");
}
@TestMetadata("propertyAnnotation.kt")
public void testPropertyAnnotation() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/propertyAnnotation.kt");
@@ -13713,6 +13713,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/inheritedJvmDefault.kt");
}
@TestMetadata("interfaceExtension.kt")
public void testInterfaceExtension() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/interfaceExtension.kt");
}
@TestMetadata("propertyAnnotation.kt")
public void testPropertyAnnotation() throws Exception {
runTest("compiler/testData/codegen/box/jvm8/defaults/compatibility/propertyAnnotation.kt");
@@ -3047,6 +3047,11 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/defaultArgs.kt");
}
@TestMetadata("simpleDiamond.kt")
public void testSimpleDiamond() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/simpleDiamond.kt");
}
@TestMetadata("simpleFunction.kt")
public void testSimpleFunction() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/jvm8/jvmDefault/compatibility/simpleFunction.kt");