[Wasm] Support external vararg

Initial implementation copies Wasm array to JS array and spreads it
This commit is contained in:
Svyatoslav Kuzmich
2023-01-09 15:32:15 +01:00
committed by Space Team
parent 99de93bbd6
commit 1564de5859
10 changed files with 402 additions and 18 deletions
@@ -211,6 +211,10 @@ class WasmSymbols(
val nullableDoubleIeee754Equals = getInternalFunction("nullableDoubleIeee754Equals")
val unsafeGetScratchRawMemory = getInternalFunction("unsafeGetScratchRawMemory")
val newJsArray = getInternalFunction("newJsArray")
val jsArrayPush = getInternalFunction("jsArrayPush")
val startCoroutineUninterceptedOrReturnIntrinsics =
(0..2).map { getInternalFunction("startCoroutineUninterceptedOrReturnIntrinsic$it") }
@@ -183,7 +183,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
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 })
if (jsFun != null && function.valueParameters.all { it.defaultValue == null && it.varargElementType == null })
return
processFunctionOrConstructor(
function = function,
@@ -241,9 +241,23 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
}
append(jsFunctionReference)
append("(")
appendParameterList(numValueParameters - numDefaultParameters, isEnd = numDefaultParameters == 0)
val numNonDefaultParamters = numValueParameters - numDefaultParameters
repeat(numNonDefaultParamters) {
if (function.valueParameters[it].isVararg) {
append("...")
}
append("p$it")
if (numDefaultParameters != 0 || it + 1 < numNonDefaultParamters)
append(", ")
}
repeat(numDefaultParameters) {
append("isDefault$it ? undefined : p${numValueParameters - numDefaultParameters + it}, ")
if (function.valueParameters[numNonDefaultParamters + it].isVararg) {
append("...")
} else {
append("isDefault$it ? undefined : ")
}
append("p${numNonDefaultParamters + it}, ")
}
append(")")
}
@@ -276,7 +290,7 @@ class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBa
if (dispatchReceiver != null) {
res.addValueParameter("_this", dispatchReceiver.type)
}
function.valueParameters.forEach { res.addValueParameter(it.name, it.type) }
function.valueParameters.forEach { res.addValueParameter(it.name, it.type).apply { varargElementType = it.varargElementType } }
// 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
@@ -23,12 +23,14 @@ import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrBody
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.util.OperatorNameConventions
import kotlin.math.absoluteValue
/**
@@ -89,8 +91,13 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
// Patch function types for Number parameters as double
function.returnType = doubleIfNumber(function.returnType)
val valueParametersAdapters = function.valueParameters.map {
it.type.kotlinToJsAdapterIfNeeded(isReturn = false)
val valueParametersAdapters = function.valueParameters.map { parameter ->
val varargElementType = parameter.varargElementType
if (varargElementType != null) {
CopyToJsArrayAdapter(parameter.type, varargElementType)
} else {
parameter.type.kotlinToJsAdapterIfNeeded(isReturn = false)
}
}
val resultAdapter =
function.returnType.jsToKotlinAdapterIfNeeded(isReturn = true)
@@ -191,6 +198,16 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
)
}
val primitivesToExternRefAdapters: Map<IrType, InteropTypeAdapter> = mapOf(
builtIns.byteType to adapters.kotlinByteToExternRefAdapter,
builtIns.shortType to adapters.kotlinShortToExternRefAdapter,
builtIns.charType to adapters.kotlinCharToExternRefAdapter,
builtIns.intType to adapters.kotlinIntToExternRefAdapter,
builtIns.longType to adapters.kotlinLongToExternRefAdapter,
builtIns.floatType to adapters.kotlinFloatToExternRefAdapter,
builtIns.doubleType to adapters.kotlinDoubleToExternRefAdapter,
).mapValues { FunctionBasedAdapter(it.value.owner) }
private fun IrType.kotlinToJsAdapterIfNeeded(isReturn: Boolean): InteropTypeAdapter? {
if (isReturn && this == builtIns.unitType)
return null
@@ -213,18 +230,9 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
)
}
val primitiveToExternRefAdapter = when (notNullType) {
builtIns.byteType -> adapters.kotlinByteToExternRefAdapter.owner
builtIns.shortType -> adapters.kotlinShortToExternRefAdapter.owner
builtIns.charType -> adapters.kotlinCharToExternRefAdapter.owner
builtIns.intType -> adapters.kotlinIntToExternRefAdapter.owner
builtIns.longType -> adapters.kotlinLongToExternRefAdapter.owner
builtIns.floatType -> adapters.kotlinFloatToExternRefAdapter.owner
builtIns.doubleType -> adapters.kotlinDoubleToExternRefAdapter.owner
else -> null
}
val primitiveToExternRefAdapter = primitivesToExternRefAdapters[notNullType]
val typeAdapter = primitiveToExternRefAdapter?.let(::FunctionBasedAdapter)
val typeAdapter = primitiveToExternRefAdapter
?: notNullType.kotlinToJsAdapterIfNeededNotNullable(isReturn)
?: return null
@@ -770,6 +778,68 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT
}
}
}
/**
* Vararg parameter adapter
*/
inner class CopyToJsArrayAdapter(
override val fromType: IrType,
private val fromElementType: IrType,
) : InteropTypeAdapter {
override val toType: IrType =
context.wasmSymbols.externalInterfaceType
private val elementAdapter =
primitivesToExternRefAdapters[fromElementType]
?: fromElementType.kotlinToJsAdapterIfNeeded(false)
private val arrayClass = fromType.classOrNull!!
private val getMethod = arrayClass.getSimpleFunction("get")!!.owner
private val sizeMethod = arrayClass.getPropertyGetter("size")!!.owner
override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
return builder.irComposite {
val originalArrayVar = irTemporary(expression)
// val newJsArray = []
// var index = 0
// while(index != size) {
// newJsArray.push(adapt(originalArray[index]));
// index++
// }
val newJsArrayVar = irTemporary(irCall(symbols.newJsArray))
val indexVar = irTemporary(irInt(0), isMutable = true)
val arraySizeVar = irTemporary(irCall(sizeMethod).apply { dispatchReceiver = irGet(originalArrayVar) })
+irWhile().apply {
condition = irNotEquals(irGet(indexVar), irGet(arraySizeVar))
body = irBlock {
val adaptedValue = elementAdapter.adaptIfNeeded(
irImplicitCast(
irCall(getMethod).apply {
dispatchReceiver = irGet(originalArrayVar)
putValueArgument(0, irGet(indexVar))
},
fromElementType
),
this@irBlock
)
+irCall(symbols.jsArrayPush).apply {
putValueArgument(0, irGet(newJsArrayVar))
putValueArgument(1, adaptedValue)
}
val inc = indexVar.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INC }
+irSet(
indexVar,
irCallOp(inc.symbol, indexVar.type, irGet(indexVar)),
origin = IrStatementOrigin.PREFIX_INCR
)
}
}
+irGet(newJsArrayVar)
}
}
}
}
internal fun StringBuilder.appendParameterList(size: Int, name: String = "p", isEnd: Boolean = true) =
+77
View File
@@ -0,0 +1,77 @@
function paramCount() {
return arguments.length
}
function Bar(size, order) {
this.size = size;
Bar.checkOrder(order);
}
Bar.order = 0;
Bar.hasOrderProblem = false;
Bar.checkOrder = function (expectedOrder) {
var curOrder = Bar.order++;
Bar.hasOrderProblem = Bar.hasOrderProblem || curOrder !== expectedOrder;
};
Bar.startNewTest = function () {
Bar.hasOrderProblem = false;
Bar.order = 0;
return true;
};
Bar.prototype.test = function (order, dummy /*, args */) {
Bar.checkOrder(order);
return dummy === 1 && (arguments.length - 2) === this.size;
};
Bar.prototype.test2 = Bar.prototype.test;
function test3(bar, dummy /*, args */) {
return dummy === 1 && (arguments.length - 2) === bar.size;
}
var obj = {
test: function (size /*, args */) {
return (arguments.length - 1) === size;
}
};
function testNativeVarargWithFunLit(/* args, f */) {
var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
var f = arguments[arguments.length - 1];
return typeof f === "function" && f(args);
}
function sumOfParameters() {
var size = arguments.length;
var result = 0;
for (var i = 0; i < size; i++) {
result += arguments[i];
}
return result;
}
function sumFunValuesOnParameters() {
var size = arguments.length - 1;
var f = arguments[arguments.length - 1];
var result = 0;
for (var i = 0; i < size; i++) {
var u = arguments[i];
result += f(u);
}
return result;
}
function idArrayVarArg() {
var args = Array.prototype.slice.call(arguments, 0, arguments.length);
return args;
}
function join(...args) {
return args.join("-");
}
function mapJoin(f, ...args) {
return args.map(x => f(x)).join("-")
}
+197
View File
@@ -0,0 +1,197 @@
// IGNORE_BACKEND_K2: JS_IR
// Partial copy of js/js.translator/testData/box/native/vararg.kt
// With some additions for concrete number types, strings and function references
package foo
import kotlin.test.assertEquals
external fun paramCount(vararg a: Int): Int = definedExternally
@JsName("paramCount")
external fun anotherParamCount(vararg a: Int): Int = definedExternally
// test spread operator
fun count(vararg a: Int) = paramCount(*a)
// test spread operator
fun anotherCount(vararg a: Int) = anotherParamCount(*a)
external fun test3(bar: Bar, dummy: Int, vararg args: Int): Boolean = definedExternally
external class Bar(size: Int, order: Int = definedExternally) {
val size: Int
fun test(order: Int, dummy: Int, vararg args: Int): Boolean = definedExternally
companion object {
fun startNewTest(): Boolean = definedExternally
var hasOrderProblem: Boolean = definedExternally
}
}
external object obj {
fun test(size: Int, vararg args: Int): Boolean = definedExternally
}
fun spreadInMethodCall(size: Int, vararg args: Int) = Bar(size).test(0, 1, *args)
fun spreadInObjectMethodCall(size: Int, vararg args: Int) = obj.test(size, *args)
fun spreadInPackageMethodCall(size: Int, vararg args: Int) = test3(Bar(size), 1, *args)
fun testSpreadOperatorWithSafeCall(a: Bar?, expected: Boolean?, vararg args: Int): Boolean {
return a?.test(0, 1, *args) == expected
}
fun testSpreadOperatorWithSureCall(a: Bar?, vararg args: Int): Boolean {
return a!!.test(0, 1, *args)
}
fun testCallOrder(vararg args: Int) =
Bar.startNewTest() &&
Bar(args.size, 0).test(1, 1, *args) && Bar(args.size, 2).test(3, 1, *args) &&
!Bar.hasOrderProblem
external fun sumOfParameters(x: Int, y: Int, vararg a: Int): Int = definedExternally
external fun sumFunValuesOnParameters(x: Int, y: Int, vararg a: Int, f: (Int) -> Int): Int = definedExternally
fun runCallable(fn: (Int, Int, Int, Int, Int) -> Int, a1: Int, a2: Int, a3: Int, a4: Int, a5: Int): Int = fn(a1, a2, a3, a4, a5)
@JsName("join")
external fun joinBoolean(vararg x: Boolean): String
@JsName("join")
external fun joinByte(vararg x: Byte): String
@JsName("join")
external fun joinChar(vararg x: Char): String
@JsName("join")
external fun joinShort(vararg x: Short): String
@JsName("join")
external fun joinInt(vararg x: Int): String
@JsName("join")
external fun joinLong(vararg x: Long): String
@JsName("join")
external fun joinFloat(vararg x: Float): String
@JsName("join")
external fun joinDouble(vararg x: Double): String
@JsName("join")
external fun joinString(vararg x: String): String
@JsName("mapJoin")
external fun mapJoin(f: ((String) -> String) -> String, vararg x: (String) -> String): String
@JsName("join")
external fun joinWithDefault(a: Int = definedExternally, b: Int = definedExternally, vararg c: Int, d: Int = definedExternally): String
fun box(): String {
if (paramCount() != 0)
return "failed when call native function without args"
if (paramCount(1) != 1) return "failed when call native function with single vararg"
if (paramCount(1, 2, 3) != 3)
return "failed when call native function with some args"
if (anotherParamCount(1, 2, 3) != 3)
return "failed when call native function with some args witch declareted with custom name"
if (count() != 0)
return "failed when call native function without args using spread operator"
if (count(1, 1, 1, 1) != 4)
return "failed when call native function with some args using spread operator"
if (anotherCount(1, 2, 3) != 3)
return "failed when call native function with some args using spread operator witch declareted with custom name"
if (!Bar(5).test(0, 1, 1, 2, 3, 4, 5))
return "failed when call method with some args"
if (!spreadInMethodCall(2, 1, 2))
return "failed when call method using spread operator"
if (!Bar(1).test(0, 1, 1))
return "failed when call method with single arg"
if (!spreadInMethodCall(2, 1, 2))
return "failed when call method using spread operator"
if (!(obj.test(5, 1, 2, 3, 4, 5)))
return "failed when call method of object"
if (!(spreadInObjectMethodCall(2, 1, 2)))
return "failed when call method of object using spread operator"
if (!spreadInPackageMethodCall(2, 1, 2))
return "failed when call package method using spread operator"
if (!(testSpreadOperatorWithSafeCall(null, null)))
return "failed when test spread operator with SafeCall (?.) using null receiver"
if (!(testSpreadOperatorWithSafeCall(Bar(3), true, 1, 2, 3)))
return "failed when test spread operator with SafeCall (?.)"
if (!(testSpreadOperatorWithSureCall(Bar(3), 1, 2, 3)))
return "failed when test spread operator with SureCall (!!)"
if (!(testCallOrder()))
return "failed when test calling order when using spread operator without args"
if (!(testCallOrder(1, 2, 3, 4)))
return "failed when test calling order when using spread operator with some args"
val baz: Bar? = Bar(1)
if (!(baz!!)?.test(0, 1, 1)!!)
return "failed when combined SureCall and SafeCall, maybe we lost cached expression"
assertEquals(45, sumOfParameters(1, 2, 3, 4, 5, 6, 7, 8, 9))
assertEquals(45, sumOfParameters(1, 2, *intArrayOf(3, 4, 5, 6, 7, 8, 9)))
assertEquals(45, sumOfParameters(1, 2, a = *intArrayOf(3, 4, 5, 6, 7, 8, 9)))
assertEquals(45, sumOfParameters(1, 2, a = intArrayOf(3, 4, 5, 6, 7, 8, 9)))
assertEquals(45, sumOfParameters(1, 2, 3, 4, *intArrayOf(5, 6, 7, 8, 9)))
assertEquals(90, sumFunValuesOnParameters(1, 2, 3, 4, 5, 6, 7, 8, 9) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, *intArrayOf(3, 4, 5, 6, 7, 8, 9)) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, a = *intArrayOf(3, 4, 5, 6, 7, 8, 9)) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, a = intArrayOf(3, 4, 5, 6, 7, 8, 9)) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, 3, 4, *intArrayOf(5, 6, 7, 8, 9)) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, *intArrayOf(3, 4, 5, 6, 7), 8, 9) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, *intArrayOf(3, 4, 5), *intArrayOf(6, 7, 8, 9)) { 2*it })
assertEquals(90, sumFunValuesOnParameters(1, 2, *intArrayOf(3, 4), 5, 6, *intArrayOf(7, 8, 9)) { 2*it })
assertEquals(5, runCallable(::paramCount, 1, 2, 3, 4, 5))
assertEquals(5, runCallable(::anotherParamCount, 1, 2, 3, 4, 5))
assertEquals(5, runCallable(::anotherCount, 1, 2, 3, 4, 5))
assertEquals(11111, runCallable(::sumOfParameters, 1, 10, 100, 1000, 10000))
if (joinBoolean(true, false, true) != "true-false-true") return "Fail1"
if (joinByte(10.toByte(), 20.toByte(), 30.toByte()) != "10-20-30") return "Fail2"
if (joinChar(10.toChar(), 20.toChar(), 30.toChar()) != "10-20-30") return "Fail3"
if (joinShort(10.toShort(), 20.toShort(), 30.toShort()) != "10-20-30") return "Fail4"
if (joinInt(10.toInt(), 20.toInt(), 30.toInt()) != "10-20-30") return "Fail5"
if (joinLong(10.toLong(), 20.toLong(), 30.toLong()) != "10-20-30") return "Fail6"
if (joinFloat(10.toFloat(), 20.toFloat(), 30.toFloat()) != "10-20-30") return "Fail7"
if (joinDouble(10.toDouble(), 20.toDouble(), 30.toDouble()) != "10-20-30") return "Fail8"
if (joinString("a", "b", "c") != "a-b-c") return "Fail9"
if (
mapJoin(
f = { f -> f("a")},
{ it + "10" },
{ it + "20" },
{ it + "30" }
) != "a10-a20-a30"
) { return "Fail10" }
if (joinWithDefault(10, 20, 30, 40, d = 50) != "10-20-30-40-50") return "Fail11"
return "OK"
}
@@ -84,4 +84,10 @@ public class FirJsCodegenWasmJsInteropTestGenerated extends AbstractFirJsCodegen
public void testTypes() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt");
}
@Test
@TestMetadata("vararg.kt")
public void testVararg() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt");
}
}
@@ -84,4 +84,10 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm
public void testTypes() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt");
}
@Test
@TestMetadata("vararg.kt")
public void testVararg() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt");
}
}
@@ -100,6 +100,11 @@ public class IrCodegenWasmJsInteropWasmTestGenerated extends AbstractIrCodegenWa
runTest("compiler/testData/codegen/boxWasmJsInterop/types.kt");
}
@TestMetadata("vararg.kt")
public void testVararg() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt");
}
@TestMetadata("wasmImport.kt")
public void testWasmImport() throws Exception {
runTest("compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt");
@@ -1,4 +1,3 @@
// IGNORE_BACKEND: WASM
// EXPECTED_REACHABLE_NODES: 1281
import Test.test
@@ -324,3 +324,9 @@ internal fun kotlinShortToExternRefAdapter(x: Short): ExternalInterfaceType =
internal fun kotlinCharToExternRefAdapter(x: Char): ExternalInterfaceType =
intToExternref(x.toInt())
@JsFun("() => []")
internal external fun newJsArray(): ExternalInterfaceType
@JsFun("(array, element) => { array.push(element); }")
internal external fun jsArrayPush(array: ExternalInterfaceType, element: ExternalInterfaceType)