From f833fc4bcba2988b00f7f53ee62f53481e5edbf5 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kuzmich Date: Wed, 17 Nov 2021 19:28:14 +0000 Subject: [PATCH] [Wasm] Default parameter values for external functions + import top-level external functions wrapped in lambdas --- .../kotlin/backend/wasm/WasmLoweringPhases.kt | 2 +- .../backend/wasm/ir2wasm/BodyGenerator.kt | 10 +- ...DeclarationsToTopLevelFunctionsLowering.kt | 137 +++++++++++++----- .../wasm/lower/JsInteropFunctionsLowering.kt | 11 +- .../codegen/boxWasmJsInterop/defaultValues.kt | 65 +++++++++ ...IrCodegenWasmJsInteropJsTestGenerated.java | 6 + ...CodegenWasmJsInteropWasmTestGenerated.java | 5 + .../kotlin/wasm/internal/ExternalWrapper.kt | 6 +- 8 files changed, 200 insertions(+), 42 deletions(-) create mode 100644 compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt index adf03b7849f..04c9b124103 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt @@ -300,7 +300,7 @@ private val addMainFunctionCallsLowering = makeCustomWasmModulePhase( ) private val defaultArgumentStubGeneratorPhase = makeWasmModulePhase( - ::DefaultArgumentStubGenerator, + { context -> DefaultArgumentStubGenerator(context, skipExternalMethods = true) }, name = "DefaultArgumentStubGenerator", description = "Generate synthetic stubs for functions with default parameter values" ) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt index 6cbef3c7236..791dc5af262 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt @@ -269,13 +269,19 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV return } + val function: IrFunction = call.symbol.owner.realOverrideTarget + call.dispatchReceiver?.let { generateExpression(it) } call.extensionReceiver?.let { generateExpression(it) } for (i in 0 until call.valueArgumentsCount) { - generateExpression(call.getValueArgument(i)!!) + val valueArgument = call.getValueArgument(i) + if (valueArgument == null) { + generateDefaultInitializerForType(context.transformType(function.valueParameters[i].type), body) + } else { + generateExpression(valueArgument) + } } - val function: IrFunction = call.symbol.owner.realOverrideTarget if (tryToGenerateIntrinsicCall(call, function)) { if (function.returnType == irBuiltIns.unitType) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt index 407a80ac7e6..f12da5865fc 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt @@ -11,6 +11,8 @@ import org.jetbrains.kotlin.backend.common.lower.createIrBuilder import org.jetbrains.kotlin.backend.wasm.WasmBackendContext import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter @@ -20,6 +22,7 @@ import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.* @@ -159,53 +162,94 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa fun processExternalConstructor(constructor: IrConstructor) { val klass = constructor.constructedClass - val jsCode = buildString { - append("(") - appendParameterList(constructor.valueParameters.size) - append(") => new ") - appendExternalClassReference(klass) - append("(") - appendParameterList(constructor.valueParameters.size) - append(")") - } - - val res = createExternalJsFunction( - klass.name, - "_\$external_constructor", - resultType = klass.defaultType, - jsCode = jsCode + processFunctionOrConstructor( + function = constructor, + name = klass.name, + returnType = klass.defaultType, + isConstructor = true, + jsFunctionReference = buildString { appendExternalClassReference(klass) } ) - - constructor.valueParameters.forEach { res.addValueParameter(it.name, it.type) } - externalFunToTopLevelMapping[constructor] = res } fun processExternalSimpleFunction(function: IrSimpleFunction) { - if (function.parent is IrPackageFragment) return - val jsName = function.getJsNameOrKotlinName() + val jsFun = function.getJsFunAnnotation() + // Wrap external functions without @JsFun to lambdas `foo` -> `(a, b) => foo(a, b)`. + // This way we wouldn't fail if we don't call them. + if (jsFun != null && function.valueParameters.all { it.defaultValue == null }) + return + processFunctionOrConstructor( + function = function, + name = function.name, + returnType = function.returnType, + isConstructor = false, + jsFunctionReference = jsFun?.let { "($it)" } ?: function.getJsNameOrKotlinName().identifier + ) + } + + fun processFunctionOrConstructor( + function: IrFunction, + name: Name, + returnType: IrType, + isConstructor: Boolean, + jsFunctionReference: String + ) { val dispatchReceiver = function.dispatchReceiverParameter - require(dispatchReceiver != null) { - "Non top-level external function w/o dispatchReceiverParameter: ${function.fqNameWhenAvailable}" - } + val numValueParameters = function.valueParameters.size + + + val numDefaultParameters = + numDefaultParametersForExternalFunction(function) val jsCode = buildString { - append("(_this, ") - appendParameterList(function.valueParameters.size) - append(") => _this.") - append(jsName) append("(") - appendParameterList(function.valueParameters.size) + if (dispatchReceiver != null) { + append("_this, ") + } + appendParameterList(numValueParameters, isEnd = numDefaultParameters == 0) + + // Parameters with default values are handled via adding additional flags to indicate that parameter is default . + // + // external fun foo(x: Int, y: Int = definedExternally, z: Int = definedExternally) + // + // => + // + // @JsCode("""(x, y, z, isDefault1, isDefault2) => + // foo( + // x, + // isDefault1 ? undefined : y, + // isDefault2 ? undefined : z + // ) + // """) + // external fun foo(x: Int, y: Int, z: Int, isDefault1: Int, isDefault: Int) + + appendParameterList(numDefaultParameters, "isDefault", isEnd = true) + append(") => ") + if (isConstructor) { + append("new ") + } + if (dispatchReceiver != null) { + append("_this.") + } + append(jsFunctionReference) + append("(") + appendParameterList(numValueParameters - numDefaultParameters, isEnd = numDefaultParameters == 0) + repeat(numDefaultParameters) { + append("isDefault$it ? undefined : p${numValueParameters - numDefaultParameters + it}, ") + } append(")") } val res = createExternalJsFunction( - function.name, - "_\$external_member_fun", - resultType = function.returnType, + name, + "_\$external_fun", + resultType = returnType, jsCode = jsCode ) - res.addValueParameter("_this", dispatchReceiver.type) + if (dispatchReceiver != null) { + res.addValueParameter("_this", dispatchReceiver.type) + } function.valueParameters.forEach { res.addValueParameter(it.name, it.type) } - + // Using Int type with 0 and 1 values to prevent overhead of converting Boolean to true and false + repeat(numDefaultParameters) { res.addValueParameter("isDefault$it", context.irBuiltIns.intType) } externalFunToTopLevelMapping[function] = res } @@ -289,11 +333,26 @@ class ComplexExternalDeclarationsUsageLowering(val context: WasmBackendContext) } fun transformCall(call: IrFunctionAccessExpression): IrExpression { + val oldFun = call.symbol.owner.realOverrideTarget val newFun: IrSimpleFunction? = - context.mapping.wasmNestedExternalToNewTopLevelFunction[call.symbol.owner.realOverrideTarget] + context.mapping.wasmNestedExternalToNewTopLevelFunction[oldFun] return if (newFun != null) { - irCall(call, newFun, receiversAsArguments = true) + val newCall = irCall(call, newFun, receiversAsArguments = true) + + // Add default arguments flags if needed + val numDefaultParameters = numDefaultParametersForExternalFunction(oldFun) + val firstDefaultFlagArgumentIdx = newFun.valueParameters.size - numDefaultParameters + val firstOldDefaultArgumentIdx = call.valueArgumentsCount - numDefaultParameters + repeat(numDefaultParameters) { + val value = if (call.getValueArgument(firstOldDefaultArgumentIdx + it) == null) 1 else 0 + newCall.putValueArgument( + firstDefaultFlagArgumentIdx + it, + IrConstImpl.int(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.intType, value) + ) + } + + newCall } else { call } @@ -302,4 +361,12 @@ class ComplexExternalDeclarationsUsageLowering(val context: WasmBackendContext) } } +private fun numDefaultParametersForExternalFunction(function: IrFunction): Int { + val firstDefaultParameterIndex: Int? = + function.valueParameters.firstOrNull { it.defaultValue != null }?.index + return if (firstDefaultParameterIndex == null) + 0 + else + function.valueParameters.size - firstDefaultParameterIndex +} diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt index 42eb5f57233..1f120626e78 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt @@ -81,6 +81,11 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT * fun foo__externalAdapter(x: KotlinType): KotlinType = adaptResult(foo(adaptParameter(x))); */ fun transformExternalFunction(function: IrSimpleFunction): List? { + // External functions with default parameter values are already processed by + // [ComplexExternalDeclarationsToTopLevelFunctionsLowering] + if (function.valueParameters.any { it.defaultValue != null }) + return null + val valueParametersAdapters = function.valueParameters.map { it.type.kotlinToJsAdapterIfNeeded(isReturn = false) } @@ -600,11 +605,11 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT } } -internal fun StringBuilder.appendParameterList(size: Int) = +internal fun StringBuilder.appendParameterList(size: Int, name: String = "p", isEnd: Boolean = true) = repeat(size) { - append("p") + append(name) append(it) - if (it + 1 < size) + if (!isEnd || it + 1 < size) append(", ") } diff --git a/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt b/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt new file mode 100644 index 00000000000..00e85d5b899 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt @@ -0,0 +1,65 @@ +// FILE: defaultValues.js +function foo(x1 = "d1", x2 = "d2", x3 = "d3", x4 = "d4", x5 = "d5") { + return `${x1} ${x2} ${x3} ${x4} ${x5}`; +} + +const foo2 = foo; + +class C { + constructor(x1 = 100, x2 = 200) { + this.x1 = x1; + this.x2 = x2; + } + + foo(x3 = 300, x4 = 400) { + return `${this.x1} ${this.x2} ${x3} ${x4}`; + } + + bar(x5 = new C(10, 20), x6 = new C(1, 2)) { + return `${x5.foo()} ${x6.foo()}`; + } +} + +// FILE: defaultValues.kt + +external fun foo( + x1: String = definedExternally, + x2: String = definedExternally, + x3: String = definedExternally, + x4: String = definedExternally, + x5: String = definedExternally, +): String + +external fun foo2( + x1: String, + x2: String, + x3: String = definedExternally, + x4: String, + x5: String = definedExternally, +): String + +external class C { + constructor(x1: Int = definedExternally, x2: Int = definedExternally) + val x1: Int + val x2: Int + fun foo(x3: Int = definedExternally, x4: Int = definedExternally): String + fun bar(x5: C = definedExternally, x6: Any = definedExternally) : String +} + +fun box(): String { + if (foo() != "d1 d2 d3 d4 d5") return "Fail 1" + if (foo(x1 = "x1", x3 = "x3", x5 = "x5") != "x1 d2 x3 d4 x5") return "Fail 2" + if (foo("x1", "x2", "x3", "x4", "x5") != "x1 x2 x3 x4 x5") return "Fail 3" + if (foo2("x1", "x2", x4 = "x4") != "x1 x2 d3 x4 d5") return "Fail 4" + if (foo2("x1", "x2", "x3", "x4", "x5") != "x1 x2 x3 x4 x5") return "Fail 5" + + if (C(1, 2).foo(3, 4) != "1 2 3 4") return "Fail 6" + if (C(1).foo(3) != "1 200 3 400") return "Fail 7" + if (C(x2 = 2).foo(x4 = 4) != "100 2 300 4") return "Fail 8" + if (C().foo() != "100 200 300 400") return "Fail 9" + + if (C().bar(C(), C()) != "100 200 300 400 100 200 300 400") return "Fail 10" + if (C().bar() != "10 20 300 400 1 2 300 400") return "Fail 11" + + return "OK" +} \ No newline at end of file diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java index 4739e49a00d..4365ac664aa 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java @@ -25,6 +25,12 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true); } + @Test + @TestMetadata("defaultValues.kt") + public void testDefaultValues() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt"); + } + @Test @TestMetadata("externalTypeOperators.kt") public void testExternalTypeOperators() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java index 985ef80e7a6..1551e8b9504 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/testOld/wasm/semantics/IrCodegenWasmJsInteropWasmTestGenerated.java @@ -30,6 +30,11 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.WASM, true); } + @TestMetadata("defaultValues.kt") + public void testDefaultValues() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/defaultValues.kt"); + } + @TestMetadata("externalTypeOperators.kt") public void testExternalTypeOperators() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/externalTypeOperators.kt"); diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt index abbcbe07eac..5fa84eaa89e 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt @@ -164,7 +164,11 @@ internal fun convertJsStringToKotlinString(x: ExternalInterfaceType): String { internal external fun jsWriteStringIntoMemory(str: ExternalInterfaceType, addr: Int) -internal fun kotlinToJsStringAdapter(x: String): ExternalInterfaceType { +internal fun kotlinToJsStringAdapter(x: String?): ExternalInterfaceType? { + // Using nullable String to represent default value + // for parameters with default values + if (x == null) + return null return importStringFromWasm(exportString(x)) }