[JS IR] Extract varargs in bridges generated for external methods

The issue this commit fixes occurs when we have an external interface
implemented by a Kotlin class, if that interface has methods with
varargs.

Kotlin functions expect varargs passed as arrays, but JavaScript code
may be unaware of this convention.

So, when generating a bridge for external interface method
implementaion, we insert some additional logic for extracting varargs
using the JavaScript `arguments` object.

A simplified example:

```kotlin
external interface Adder {
    fun sum(vararg numbers: Int): Int
}

class AdderImpl: Adder {
    override fun sum(vararg numbers: Int) = numbers.sum()
}
```

For `AdderImpl` we generate the following JS code:
```js
AdderImpl.prototype.sum_69wd7h_k$ = function (numbers) {
  return sum(numbers);
};
AdderImpl.prototype.sum = function () {
  var numbers = new Int32Array([].slice.call(arguments));
  return this.sum_69wd7h_k$(numbers);
};
```

#KT-15223 Fixed
This commit is contained in:
Sergej Jaskiewicz
2021-12-22 20:56:19 +03:00
committed by Space
parent d5e46e0607
commit 4a724611d8
7 changed files with 439 additions and 116 deletions
@@ -13,41 +13,54 @@ import org.jetbrains.kotlin.backend.common.lower.*
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsLoweredDeclarationOrigin
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.varargParameterIndex
import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.util.*
// Constructs bridges for inherited generic functions
//
// Example: for given class hierarchy
//
// class C<T> {
// fun foo(t: T) = ...
// }
//
// class D : C<Int> {
// override fun foo(t: Int) = impl
// }
//
// it adds method D that delegates generic calls to implementation:
//
// class D : C<Int> {
// override fun foo(t: Int) = impl
// fun foo(t: Any?) = foo(t as Int) // Constructed bridge
// }
//
/**
* Constructs bridges for inherited generic functions
*
* Example: for given class hierarchy
*
* class C<T> {
* fun foo(t: T) = ...
* }
*
* class D : C<Int> {
* override fun foo(t: Int) = impl
* }
*
* it adds method D that delegates generic calls to implementation:
*
* class D : C<Int> {
* override fun foo(t: Int) = impl
* fun foo(t: Any?) = foo(t as Int) // Constructed bridge
* }
*/
abstract class BridgesConstruction<T : JsCommonBackendContext>(val context: T) : DeclarationTransformer {
private val specialBridgeMethods = SpecialBridgeMethods(context)
abstract fun getFunctionSignature(function: IrSimpleFunction): Any
/**
* Usually just returns [irFunction]'s value parameters, but special transformations may be required if,
* for example, we're dealing with an external function, and that function contains a vararg,
* which we must extract and convert to an array.
*/
protected open fun extractValueParameters(
blockBodyBuilder: IrBlockBodyBuilder,
irFunction: IrSimpleFunction,
bridge: IrSimpleFunction
): List<IrValueDeclaration> = irFunction.valueParameters
// Should dispatch receiver type be casted inside a bridge.
open val shouldCastDispatchReceiver: Boolean = false
@@ -131,7 +144,7 @@ abstract class BridgesConstruction<T : JsCommonBackendContext>(val context: T) :
copyTypeParametersFrom(bridge)
val substitutionMap = makeTypeParameterSubstitutionMap(bridge, this)
copyReceiverParametersFrom(bridge, substitutionMap)
valueParameters += bridge.valueParameters.map { p -> p.copyTo(this, type = p.type.substitute(substitutionMap)) }
copyValueParametersFrom(bridge, substitutionMap)
annotations += bridge.annotations
overriddenSymbols += delegateTo.overriddenSymbols
overriddenSymbols += bridge.symbol
@@ -139,11 +152,12 @@ abstract class BridgesConstruction<T : JsCommonBackendContext>(val context: T) :
irFunction.body = context.irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET) {
statements += context.createIrBuilder(irFunction.symbol).irBlockBody(irFunction) {
val valueParameters = extractValueParameters(this, irFunction, bridge)
if (specialMethodInfo != null) {
irFunction.valueParameters.take(specialMethodInfo.argumentsToCheck).forEach {
valueParameters.take(specialMethodInfo.argumentsToCheck).forEachIndexed { index, valueDeclaration ->
+irIfThen(
context.irBuiltIns.unitType,
irNot(irIs(irGet(it), delegateTo.valueParameters[it.index].type)),
irNot(irIs(irGet(valueDeclaration), delegateTo.valueParameters[index].type)),
irReturn(specialMethodInfo.defaultValueGenerator(irFunction))
)
}
@@ -161,9 +175,9 @@ abstract class BridgesConstruction<T : JsCommonBackendContext>(val context: T) :
call.extensionReceiver = irCastIfNeeded(irGet(it), delegateTo.extensionReceiverParameter!!.type)
}
val toTake = irFunction.valueParameters.size - if (call.isSuspend xor irFunction.isSuspend) 1 else 0
val toTake = valueParameters.size - if (call.isSuspend xor irFunction.isSuspend) 1 else 0
irFunction.valueParameters.subList(0, toTake).mapIndexed { i, valueParameter ->
valueParameters.subList(0, toTake).mapIndexed { i, valueParameter ->
call.putValueArgument(i, irCastIfNeeded(irGet(valueParameter), delegateTo.valueParameters[i].type))
}
@@ -174,6 +188,22 @@ abstract class BridgesConstruction<T : JsCommonBackendContext>(val context: T) :
return irFunction
}
/**
* Copies the value parameters from [bridge] to [this]. If [bridge] is external and contains a vararg parameter,
* only copies the parameters before the vararg.
* The rest parameters are expected to be obtained later using the `arguments` object in JS.
*/
private fun IrSimpleFunction.copyValueParametersFrom(bridge: IrSimpleFunction, substitutionMap: Map<IrTypeParameterSymbol, IrType>) {
var valueParametersToCopy = bridge.valueParameters
if (bridge.isEffectivelyExternal()) {
val varargIndex = bridge.varargParameterIndex()
if (varargIndex != -1) {
valueParametersToCopy = bridge.valueParameters.take(varargIndex)
}
}
valueParameters += valueParametersToCopy.map { p -> p.copyTo(this, type = p.type.substitute(substitutionMap)) }
}
abstract fun getBridgeOrigin(bridge: IrSimpleFunction): IrDeclarationOrigin
// TODO: get rid of Unit check
@@ -213,6 +243,3 @@ data class IrBasedFunctionHandle(val function: IrSimpleFunction) : FunctionHandl
override fun getOverridden() =
function.overriddenSymbols.map { IrBasedFunctionHandle(it.owner) }
}
@@ -7,18 +7,34 @@ package org.jetbrains.kotlin.ir.backend.js.lower
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsLoweredDeclarationOrigin
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrArithBuilder
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.varargParameterIndex
import org.jetbrains.kotlin.ir.backend.js.utils.eraseGenerics
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.hasStableJsName
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.types.isUnit
import org.jetbrains.kotlin.ir.util.isEffectivelyExternal
import org.jetbrains.kotlin.ir.util.render
import org.jetbrains.kotlin.name.Name
class JsBridgesConstruction(context: JsIrBackendContext) : BridgesConstruction<JsIrBackendContext>(context) {
private val calculator = JsIrArithBuilder(context)
private val jsArguments = context.intrinsics.jsArguments
private val jsArrayGet = context.intrinsics.jsArrayGet
private val jsArrayLength = context.intrinsics.jsArrayLength
private val jsArrayLike2Array = context.intrinsics.jsArrayLike2Array
private val jsSliceArrayLikeFromIndex = context.intrinsics.jsSliceArrayLikeFromIndex
private val jsSliceArrayLikeFromIndexToIndex = context.intrinsics.jsSliceArrayLikeFromIndexToIndex
private val primitiveArrays = context.intrinsics.primitiveArrays
private val primitiveToLiteralConstructor = context.intrinsics.primitiveToLiteralConstructor
private fun IrType.getJsInlinedClass() = context.inlineClassesUtils.getInlinedClass(this)
override fun getFunctionSignature(function: IrSimpleFunction): JsSignature =
@@ -40,6 +56,115 @@ class JsBridgesConstruction(context: JsIrBackendContext) : BridgesConstruction<J
JsLoweredDeclarationOrigin.BRIDGE_WITH_STABLE_NAME
else
JsLoweredDeclarationOrigin.BRIDGE_WITHOUT_STABLE_NAME
override fun extractValueParameters(
blockBodyBuilder: IrBlockBodyBuilder,
irFunction: IrSimpleFunction,
bridge: IrSimpleFunction
): List<IrValueDeclaration> {
if (!bridge.isEffectivelyExternal())
return super.extractValueParameters(blockBodyBuilder, irFunction, bridge)
val varargIndex = bridge.varargParameterIndex()
if (varargIndex == -1)
return super.extractValueParameters(blockBodyBuilder, irFunction, bridge)
return blockBodyBuilder.run {
// The number of parameters after the vararg
val numberOfTrailingParameters = bridge.valueParameters.size - (varargIndex + 1)
val getTotalNumberOfArguments = irCall(jsArrayLength).apply {
putValueArgument(0, irCall(jsArguments))
type = context.irBuiltIns.intType
}
val firstTrailingParameterIndexVar = lazy {
irTemporary(
if (numberOfTrailingParameters == 0)
getTotalNumberOfArguments
else
calculator.sub(
getTotalNumberOfArguments,
irInt(numberOfTrailingParameters)
),
nameHint = "firstTrailingParameterIndex"
)
}
val varargArrayVar = emitCopyVarargToArray(
bridge,
varargIndex,
numberOfTrailingParameters,
firstTrailingParameterIndexVar
)
val trailingParametersVars = createVarsForTrailingParameters(bridge, numberOfTrailingParameters, firstTrailingParameterIndexVar)
irFunction.valueParameters + varargArrayVar + trailingParametersVars
}
}
private fun IrBlockBodyBuilder.createVarsForTrailingParameters(
bridge: IrSimpleFunction,
numberOfTrailingParameters: Int,
firstTrailingParameterIndexVar: Lazy<IrVariable>
) = bridge.valueParameters.takeLast(numberOfTrailingParameters).mapIndexed { index, trailingParameter ->
val parameterIndex = if (index == 0)
irGet(firstTrailingParameterIndexVar.value)
else
calculator.add(irGet(firstTrailingParameterIndexVar.value), irInt(index))
createTmpVariable(
irCall(jsArrayGet).apply {
putValueArgument(0, irCall(jsArguments))
putValueArgument(1, parameterIndex)
},
nameHint = trailingParameter.name.asString(),
irType = trailingParameter.type
)
}
private fun IrBlockBodyBuilder.emitCopyVarargToArray(
bridge: IrSimpleFunction,
varargIndex: Int,
numberOfTrailingParameters: Int,
firstTrailingParameterIndexVar: Lazy<IrVariable>
): IrVariable {
val varargElement = bridge.valueParameters[varargIndex]
val sliceIntrinsicArgs = mutableListOf<IrExpression>(irCall(jsArguments))
var sliceIntrinsic = jsArrayLike2Array
if (varargIndex != 0 || numberOfTrailingParameters > 0) {
sliceIntrinsicArgs.add(irInt(varargIndex))
sliceIntrinsic = jsSliceArrayLikeFromIndex
}
if (numberOfTrailingParameters > 0) {
sliceIntrinsicArgs.add(irGet(firstTrailingParameterIndexVar.value))
sliceIntrinsic = jsSliceArrayLikeFromIndexToIndex
}
val varargCopiedAsArray = irCall(sliceIntrinsic).apply {
putTypeArgument(0, varargElement.varargElementType!!)
sliceIntrinsicArgs.forEachIndexed(this::putValueArgument)
}.let { arrayExpr ->
val arrayInfo =
InlineClassArrayInfo(this@JsBridgesConstruction.context, varargElement.varargElementType!!, varargElement.type)
val primitiveType = primitiveArrays[arrayInfo.primitiveArrayType.classifierOrNull]
if (primitiveType != null) {
arrayInfo.boxArrayIfNeeded(
irCall(primitiveToLiteralConstructor.getValue(primitiveType)).apply {
putValueArgument(0, arrayExpr)
type = varargElement.type
}
)
} else {
arrayExpr
}
}
return createTmpVariable(varargCopiedAsArray, nameHint = varargElement.name.asString())
}
}
interface JsSignature {
@@ -6,8 +6,10 @@
package org.jetbrains.kotlin.ir.backend.js.lower
import org.jetbrains.kotlin.backend.common.BodyLoweringPass
import org.jetbrains.kotlin.backend.common.compilationException
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
@@ -19,7 +21,6 @@ import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.getInlineClassBackingField
import org.jetbrains.kotlin.ir.util.getInlineClassUnderlyingType
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
@@ -34,92 +35,15 @@ private class VarargTransformer(
val context: JsIrBackendContext
) : IrElementTransformerVoid() {
fun IrType.getInlinedClass() = context.inlineClassesUtils.getInlinedClass(this)
var externalVarargs = mutableSetOf<IrVararg>()
private fun List<IrExpression>.toArrayLiteral(type: IrType, varargElementType: IrType): IrExpression {
// TODO: Use symbols when builtins symbol table is fixes
val primitiveType = context.intrinsics.primitiveArrays.mapKeys { it.key }[type.classifierOrNull]
val intrinsic =
if (primitiveType != null)
context.intrinsics.primitiveToLiteralConstructor.getValue(primitiveType)
else
context.intrinsics.arrayLiteral
val startOffset = firstOrNull()?.startOffset ?: UNDEFINED_OFFSET
val endOffset = lastOrNull()?.endOffset ?: UNDEFINED_OFFSET
val irVararg = IrVarargImpl(startOffset, endOffset, type, varargElementType, this)
return IrCallImpl(
startOffset, endOffset,
type, intrinsic,
typeArgumentsCount = if (intrinsic.owner.typeParameters.isNotEmpty()) 1 else 0,
valueArgumentsCount = 1
).apply {
if (typeArgumentsCount == 1) {
putTypeArgument(0, varargElementType)
}
putValueArgument(0, irVararg)
}
}
inner class InlineClassArrayInfo(
val elementType: IrType,
val arrayType: IrType
) {
val arrayInlineClass = arrayType.getInlinedClass()
val inlined = arrayInlineClass != null
val primitiveElementType = when {
inlined -> getInlineClassUnderlyingType(elementType.getInlinedClass()!!)
else -> elementType
}
val primitiveArrayType = when {
inlined -> getInlineClassUnderlyingType(arrayInlineClass!!)
else -> arrayType
}
fun boxArrayIfNeeded(array: IrExpression) =
if (arrayInlineClass == null)
array
else with(array) {
IrConstructorCallImpl.fromSymbolOwner(
startOffset,
endOffset,
arrayInlineClass.defaultType,
arrayInlineClass.constructors.single { it.isPrimary }.symbol,
arrayInlineClass.typeParameters.size
).also {
it.putValueArgument(0, array)
}
}
fun unboxElementIfNeeded(element: IrExpression): IrExpression {
if (arrayInlineClass == null)
return element
else with(element) {
val inlinedClass = type.getInlinedClass() ?: return element
val field = getInlineClassBackingField(inlinedClass)
return IrGetFieldImpl(startOffset, endOffset, field.symbol, field.type, this)
}
}
fun toPrimitiveArrayLiteral(elements: List<IrExpression>) =
elements.toArrayLiteral(primitiveArrayType, primitiveElementType)
}
override fun visitVararg(expression: IrVararg): IrExpression {
expression.transformChildrenVoid(this)
val currentList = mutableListOf<IrExpression>()
val segments = mutableListOf<IrExpression>()
val arrayInfo = InlineClassArrayInfo(expression.varargElementType, expression.type)
val arrayInfo = InlineClassArrayInfo(context, expression.varargElementType, expression.type)
for (e in expression.elements) {
when (e) {
@@ -162,6 +86,7 @@ private class VarargTransformer(
val arrayLiteral =
segments.toArrayLiteral(
context,
IrSimpleTypeImpl(context.intrinsics.array, false, emptyList(), emptyList()), // TODO: Substitution
context.irBuiltIns.anyType
)
@@ -218,7 +143,7 @@ private class VarargTransformer(
} else segment
}
private fun transformFunctionAccessExpression(expression: IrFunctionAccessExpression): IrExpression {
override fun visitFunctionAccess(expression: IrFunctionAccessExpression): IrExpression {
expression.transformChildrenVoid()
if (expression.symbol.owner.isExternal) {
@@ -240,7 +165,7 @@ private class VarargTransformer(
val parameter = expression.symbol.owner.valueParameters[i]
val varargElementType = parameter.varargElementType
if (argument == null && varargElementType != null) {
val arrayInfo = InlineClassArrayInfo(varargElementType, parameter.type)
val arrayInfo = InlineClassArrayInfo(context, varargElementType, parameter.type)
val emptyArray = with(arrayInfo) {
boxArrayIfNeeded(toPrimitiveArrayLiteral(emptyList()))
}
@@ -251,7 +176,85 @@ private class VarargTransformer(
return expression
}
override fun visitFunctionAccess(expression: IrFunctionAccessExpression) =
transformFunctionAccessExpression(expression)
}
private fun List<IrExpression>.toArrayLiteral(context: JsIrBackendContext, type: IrType, varargElementType: IrType): IrExpression {
// TODO: Use symbols when builtins symbol table is fixes
val primitiveType = context.intrinsics.primitiveArrays.mapKeys { it.key }[type.classifierOrNull]
val intrinsic =
if (primitiveType != null)
context.intrinsics.primitiveToLiteralConstructor.getValue(primitiveType)
else
context.intrinsics.arrayLiteral
val startOffset = firstOrNull()?.startOffset ?: UNDEFINED_OFFSET
val endOffset = lastOrNull()?.endOffset ?: UNDEFINED_OFFSET
val irVararg = IrVarargImpl(startOffset, endOffset, type, varargElementType, this)
return IrCallImpl(
startOffset, endOffset,
type, intrinsic,
typeArgumentsCount = if (intrinsic.owner.typeParameters.isNotEmpty()) 1 else 0,
valueArgumentsCount = 1
).apply {
if (typeArgumentsCount == 1) {
putTypeArgument(0, varargElementType)
}
putValueArgument(0, irVararg)
}
}
internal class InlineClassArrayInfo(val context: JsIrBackendContext, val elementType: IrType, val arrayType: IrType) {
private fun IrType.getInlinedClass() = context.inlineClassesUtils.getInlinedClass(this)
private fun getInlineClassUnderlyingType(irClass: IrClass) = context.inlineClassesUtils.getInlineClassUnderlyingType(irClass)
val arrayInlineClass = arrayType.getInlinedClass()
val inlined = arrayInlineClass != null
val primitiveElementType = when {
inlined -> getInlineClassUnderlyingType(
elementType.getInlinedClass() ?: compilationException(
"Could not get inlined class",
elementType
)
)
else -> elementType
}
val primitiveArrayType = when {
inlined -> getInlineClassUnderlyingType(arrayInlineClass!!)
else -> arrayType
}
fun boxArrayIfNeeded(array: IrExpression) =
if (arrayInlineClass == null)
array
else with(array) {
IrConstructorCallImpl.fromSymbolOwner(
startOffset,
endOffset,
arrayInlineClass.defaultType,
arrayInlineClass.constructors.single { it.isPrimary }.symbol,
arrayInlineClass.typeParameters.size
).also {
it.putValueArgument(0, array)
}
}
fun unboxElementIfNeeded(element: IrExpression): IrExpression {
if (arrayInlineClass == null)
return element
else with(element) {
val inlinedClass = type.getInlinedClass() ?: return element
val field = getInlineClassBackingField(inlinedClass)
return IrGetFieldImpl(startOffset, endOffset, field.symbol, field.type, this)
}
}
fun toPrimitiveArrayLiteral(elements: List<IrExpression>) =
elements.toArrayLiteral(context, primitiveArrayType, primitiveElementType)
}
@@ -319,6 +319,9 @@ fun argumentsWithVarargAsSingleArray(
}
}
/**
* Returns the index of the vararg parameter of the function if there is one, otherwise returns -1.
*/
fun IrFunction.varargParameterIndex() = valueParameters.indexOfFirst { it.varargElementType != null }
fun translateCallArguments(
@@ -1,11 +1,11 @@
open class A<T> {
open fun foo(t: T) = "A"
open fun foo(t: T, vararg xs: Int) = "A"
}
open class B : A<String>()
class Z : B() {
override fun foo(t: String) = "Z"
override fun foo(t: String, vararg xs: Int) = "Z"
}