JS: support SAM conversion

The SAM adapter is generate on declaration site. This is different
from the JVM approach.

`external fun interface` is banned for now.

Reusing interface declaration for the adapter is a hack which
reduces code size and makes importing/exporting the adapter
effortless.
This commit is contained in:
Anton Bannykh
2020-02-14 16:12:50 +03:00
parent 2742e643c1
commit 23e218396e
18 changed files with 249 additions and 7 deletions
@@ -1,6 +1,5 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// IGNORE_BACKEND: JS
// SKIP_DCE_DRIVEN
fun interface Foo {
@@ -0,0 +1,33 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND: JVM, JVM_IR, JS_IR
// IGNORE_BACKEND_FIR: JVM_IR
// SKIP_DCE_DRIVEN
fun interface Base {
fun doStuff(): String
}
fun interface I : Base
fun interface Proxy : I {
override fun doStuff(): String = doStuffInt().toString()
fun doStuffInt(): Int
}
fun runBase(b: Base) = b.doStuff()
fun runI(i: I) = i.doStuff()
fun runProxy(p: Proxy) = p.doStuff()
fun box(): String {
if (runI { "i" } != "i") return "fail1"
if (runProxy { 10 } != "10") return "fail2"
return runBase { "OK" }
}
@@ -0,0 +1,32 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// SKIP_DCE_DRIVEN
// MODULE: m1
// FILE: m1.kt
fun interface I {
fun f(): String
}
fun rn(i: I) = i.f()
inline fun rnInline(i: I) = i.f()
// MODULE: m2(m1)
// FILE: m2.kt
fun rn2(f: () -> String) = rn(f)
inline fun rn2Inline(noinline f: () -> String) = rnInline(f)
// MODULE: main(m2)
// FILE: main.kt
fun box(): String {
if (rn2Inline { "inline" } != "inline") return "fail"
return rn2 { "OK" }
}
@@ -1,6 +1,5 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// IGNORE_BACKEND: JS
// WITH_RUNTIME
// SKIP_DCE_DRIVEN
@@ -1,6 +1,5 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// IGNORE_BACKEND: JS
// WITH_RUNTIME
// SKIP_DCE_DRIVEN
@@ -0,0 +1,20 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// SKIP_DCE_DRIVEN
// This test should check argument coercion between the SAM and the lambda.
// For now it checks that Char is boxed in JS
fun interface CharToAny {
fun invoke(c: Char): Any
}
fun foo(c: CharToAny): Any = c.invoke('O')
fun box(): String {
if (foo { it } !is Char) return "fail"
return "OK"
}
@@ -1,6 +1,5 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// IGNORE_BACKEND: JS
// WITH_RUNTIME
// SKIP_DCE_DRIVEN
@@ -1,11 +1,11 @@
// !LANGUAGE: +NewInference +FunctionalInterfaceConversion +SamConversionPerArgument +SamConversionForKotlinFunctions
// IGNORE_BACKEND_FIR: JVM_IR
// IGNORE_BACKEND: JS, JS_IR
// IGNORE_BACKEND: JS_IR
// WITH_COROUTINES
// WITH_RUNTIME
import helpers.*
import kotlin.coroutines.startCoroutine
import kotlin.coroutines.*
fun interface SuspendRunnable {
suspend fun invoke()
@@ -17,11 +17,25 @@ fun run(r: SuspendRunnable) {
var result = "initial"
var resumingCallback: () -> Unit = {}
suspend fun bar() {
// Generate proper state machine
suspendCoroutine<Unit> { cont ->
resumingCallback = {
cont.resume(Unit)
}
}
result = "OK"
}
fun box(): String {
run(::bar)
if (result != "initial") return "fail"
resumingCallback()
return result
}
@@ -11723,11 +11723,21 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
runTest("compiler/testData/codegen/box/funInterface/castFromAny.kt");
}
@TestMetadata("funInterfaceInheritance.kt")
public void testFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
@TestMetadata("inlinedSamWrapper.kt")
public void testInlinedSamWrapper() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/inlinedSamWrapper.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -11738,6 +11748,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -11700,6 +11700,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class FunInterface extends AbstractLightAnalysisModeTest {
@TestMetadata("funInterfaceInheritance.kt")
public void ignoreFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath);
}
@@ -11728,6 +11733,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
runTest("compiler/testData/codegen/box/funInterface/inlinedSamWrapper.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -11738,6 +11748,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -10593,11 +10593,21 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/funInterface/castFromAny.kt");
}
@TestMetadata("funInterfaceInheritance.kt")
public void testFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
@TestMetadata("inlinedSamWrapper.kt")
public void testInlinedSamWrapper() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/inlinedSamWrapper.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -10608,6 +10618,11 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -10593,11 +10593,21 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/funInterface/castFromAny.kt");
}
@TestMetadata("funInterfaceInheritance.kt")
public void testFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
@TestMetadata("inlinedSamWrapper.kt")
public void testInlinedSamWrapper() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/inlinedSamWrapper.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -10608,6 +10618,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -9073,6 +9073,16 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/funInterface/basicFunInterfaceConversion.kt");
}
@TestMetadata("funInterfaceInheritance.kt")
public void testFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -9083,6 +9093,11 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -9073,6 +9073,16 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/funInterface/basicFunInterfaceConversion.kt");
}
@TestMetadata("funInterfaceInheritance.kt")
public void testFunInterfaceInheritance() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/funInterfaceInheritance.kt");
}
@TestMetadata("multimodule.kt")
public void testMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/multimodule.kt");
}
@TestMetadata("nullableSam.kt")
public void testNullableSam() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/nullableSam.kt");
@@ -9083,6 +9093,11 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/funInterface/partialSam.kt");
}
@TestMetadata("primitiveConversions.kt")
public void testPrimitiveConversions() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/primitiveConversions.kt");
}
@TestMetadata("receiverEvaluatedOnce.kt")
public void testReceiverEvaluatedOnce() throws Exception {
runTest("compiler/testData/codegen/box/funInterface/receiverEvaluatedOnce.kt");
@@ -131,6 +131,8 @@ public final class Namer {
public static final String IMPORTS_FOR_INLINE_PROPERTY = "$$importsForInline$$";
public static final String SAM_FIELD_NAME = "function$";
@NotNull
public static String getFunctionTag(@NotNull CallableDescriptor functionDescriptor, @NotNull JsConfig config) {
String intrinsicTag = ArrayFIF.INSTANCE.getTag(functionDescriptor, config);
@@ -119,6 +119,8 @@ class ClassTranslator private constructor(
enumInitFunction.body.statements += JsAstUtils.asSyntheticStatement(initInvocation.source(companionDescriptor.source.getPsi()))
}
maybeConvertInterfaceToSamAdapter(context, constructorFunction)
translatePrimaryConstructor(constructorFunction, context, delegationTranslator)
addMetadataObject()
addMetadataType()
@@ -562,6 +564,42 @@ class ClassTranslator private constructor(
}
}
private fun maybeConvertInterfaceToSamAdapter(context: TranslationContext, constructorFunction: JsFunction) {
if (!descriptor.isFun) return
val paramName = context.scope().declareFreshName("f")
constructorFunction.parameters += JsParameter(paramName)
constructorFunction.body.statements += JsBinaryOperation(
JsBinaryOperator.ASG,
pureFqn(Namer.SAM_FIELD_NAME, JsThisRef()),
JsNameRef(paramName)
).makeStmt()
val samDescriptor = descriptor.unsubstitutedMemberScope
.getContributedDescriptors(DescriptorKindFilter.FUNCTIONS)
.filterIsInstance<FunctionDescriptor>()
.single { it.modality === Modality.ABSTRACT }
val function = context.getFunctionObject(samDescriptor)
val innerContext = context.newDeclaration(samDescriptor).translateAndAliasParameters(samDescriptor, function.parameters)
if (samDescriptor.isSuspend) {
function.fillCoroutineMetadata(innerContext, samDescriptor, hasController = false)
}
val parameters = listOfNotNull(samDescriptor.extensionReceiverParameter) +
samDescriptor.valueParameters +
listOfNotNull(innerContext.continuationParameterDescriptor)
val arguments = parameters.map {
TranslationUtils.coerce(innerContext, innerContext.getAliasForDescriptor(it)!!, context.currentModule.builtIns.anyType)
}
function.body = JsBlock(JsReturn(JsInvocation(pureFqn(Namer.SAM_FIELD_NAME, JsThisRef()), arguments)))
context.addDeclarationStatement(context.addFunctionToPrototype(descriptor, samDescriptor, function))
}
private fun <T : JsNode> T.withDefaultLocation(): T = apply { source = classDeclaration }
companion object {
@@ -66,7 +66,9 @@ fun TranslationContext.translateAndAliasParameters(
for (valueParameter in descriptor.valueParameters) {
val name = getNameForDescriptor(valueParameter)
val tmpName = JsScope.declareTemporaryName(name.ident).also { it.descriptor = valueParameter }
aliases[valueParameter] = JsAstUtils.pureFqn(tmpName, null)
val parameterRef = JsAstUtils.pureFqn(tmpName, null)
parameterRef.type = valueParameter.type
aliases[valueParameter] = parameterRef
targetList += JsParameter(tmpName).apply { hasDefaultValue = valueParameter.hasDefaultValue() }
}
@@ -545,6 +545,21 @@ public final class TranslationUtils {
}
}
// SAM conversion
if ((FunctionTypesKt.isFunctionTypeOrSubtype(from) || FunctionTypesKt.isSuspendFunctionTypeOrSubtype(from))) {
ClassifierDescriptor d = to.getConstructor().getDeclarationDescriptor();
if (d instanceof ClassDescriptor && ((ClassDescriptor)d).isFun()) {
JsName constructorName = context.getInlineableInnerNameForDescriptor(d.getOriginal());
if (to.isMarkedNullable()) {
JsConditional c = TranslationUtils.notNullConditional(value, new JsNullLiteral(), context);
c.setThenExpression(new JsNew(new JsNameRef(constructorName), Collections.singletonList(c.getThenExpression())));
value = c;
} else {
value = new JsNew(new JsNameRef(constructorName), Collections.singletonList(value));
}
}
}
MetadataProperties.setType(value, to);
return value;
}