[Wasm] Default parameter values for external functions
+ import top-level external functions wrapped in lambdas
This commit is contained in:
committed by
Space
parent
f8262a2549
commit
f833fc4bcb
+1
-1
@@ -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"
|
||||
)
|
||||
|
||||
+8
-2
@@ -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)
|
||||
|
||||
+102
-35
@@ -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
|
||||
}
|
||||
|
||||
+8
-3
@@ -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"
|
||||
}
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
+5
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user