[Wasm] Default parameter values for external functions

+ import top-level external functions wrapped in lambdas
This commit is contained in:
Svyatoslav Kuzmich
2021-11-17 19:28:14 +00:00
committed by Space
parent f8262a2549
commit f833fc4bcb
8 changed files with 200 additions and 42 deletions
@@ -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"
)
@@ -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)
@@ -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
}
@@ -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<IrDeclaration>? {
// 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(", ")
}
@@ -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"
}
@@ -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 {
@@ -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");
@@ -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))
}