[JS IR] Lift lambdas that capture no context

If a lambda expression does not capture any local variables, convert
it to a global free function and replace the lambda creation with
a reference to that function.

Example: for the following Kotlin code

```kotlin
fun foo(f: () -> Unit) = f()

fun bar() = foo { console.log("hello") }
```

before this patch, we generated:

```js
function foo(f) {
  return f();
}
function bar() {
  return foo(bar$lambda());
}
function bar$lambda() {
  return function () {
    console.log('hello');
  };
}
```

after this patch, we generate:

```js
function foo(f) {
  return f();
}
function bar() {
  return foo(bar$lambda);
}
function bar$lambda() {
  console.log('hello');
}
```
This commit is contained in:
Sergej Jaskiewicz
2022-04-04 14:29:54 +03:00
committed by Space
parent 026f292f4f
commit d7d86a0e95
6 changed files with 167 additions and 79 deletions
@@ -34,38 +34,66 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
override fun lower(irFile: IrFile) { override fun lower(irFile: IrFile) {
val ctorToFactoryMap = mutableMapOf<IrConstructorSymbol, IrSimpleFunctionSymbol>() val ctorToFactoryMap = mutableMapOf<IrConstructorSymbol, IrSimpleFunctionSymbol>()
irFile.transform(CallableReferenceClassTransformer(ctorToFactoryMap), null) val ctorToFreeFunctionMap = mutableMapOf<IrConstructorSymbol, IrSimpleFunctionSymbol>()
irFile.transform(CallableReferenceClassTransformer(ctorToFactoryMap, ctorToFreeFunctionMap), null)
irFile.transformChildrenVoid(object : IrElementTransformerVoid() { irFile.transformChildrenVoid(object : IrElementTransformerVoid() {
override fun visitConstructorCall(expression: IrConstructorCall): IrExpression { override fun visitConstructorCall(expression: IrConstructorCall): IrExpression {
expression.transformChildrenVoid() expression.transformChildrenVoid()
if (expression.origin != JsStatementOrigins.CALLABLE_REFERENCE_CREATE) return expression if (expression.origin != JsStatementOrigins.CALLABLE_REFERENCE_CREATE) return expression
return ctorToFactoryMap[expression.symbol]?.let { factory ->
val newCall = expression.run {
IrCallImpl(startOffset, endOffset, type, factory, typeArgumentsCount, valueArgumentsCount, origin)
}
newCall.dispatchReceiver = expression.dispatchReceiver ctorToFreeFunctionMap[expression.symbol]?.let { liftedLambda ->
newCall.extensionReceiver = expression.extensionReceiver return replaceLambdaConstructorCallWithReferenceToLiftedLambda(expression, liftedLambda)
}
for (i in 0 until expression.typeArgumentsCount) { ctorToFactoryMap[expression.symbol]?.let { factory ->
newCall.putTypeArgument(i, expression.getTypeArgument(i)) return replaceLambdaConstructorCallWithFactoryCall(expression, factory)
} }
for (i in 0 until expression.valueArgumentsCount) { return expression
newCall.putValueArgument(i, expression.getValueArgument(i))
}
newCall
} ?: expression
} }
}) })
} }
private fun replaceLambdaConstructorCallWithFactoryCall(
expression: IrConstructorCall,
factory: IrSimpleFunctionSymbol
): IrCall {
val newCall = expression.run {
IrCallImpl(startOffset, endOffset, type, factory, typeArgumentsCount, valueArgumentsCount, origin)
}
newCall.dispatchReceiver = expression.dispatchReceiver
newCall.extensionReceiver = expression.extensionReceiver
for (i in 0 until expression.typeArgumentsCount) {
newCall.putTypeArgument(i, expression.getTypeArgument(i))
}
for (i in 0 until expression.valueArgumentsCount) {
newCall.putValueArgument(i, expression.getValueArgument(i))
}
return newCall
}
private fun replaceLambdaConstructorCallWithReferenceToLiftedLambda(
expression: IrConstructorCall,
liftedLambda: IrSimpleFunctionSymbol
): IrRawFunctionReference = IrRawFunctionReferenceImpl(
expression.startOffset,
expression.endOffset,
expression.type,
liftedLambda,
)
override fun lower(irBody: IrBody, container: IrDeclaration) { override fun lower(irBody: IrBody, container: IrDeclaration) {
compilationException("Unreachable", irBody) compilationException("Unreachable", irBody)
} }
private inner class CallableReferenceClassTransformer(private val ctorToFactoryMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol>) : IrElementTransformerVoid() { private inner class CallableReferenceClassTransformer(
private val ctorToFactoryMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol>,
private val ctorToFreeFunctionMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol>
) : IrElementTransformerVoid() {
override fun visitFile(declaration: IrFile): IrFile { override fun visitFile(declaration: IrFile): IrFile {
declaration.transformChildrenVoid() declaration.transformChildrenVoid()
declaration.transformDeclarationsFlat { it.transformCallableReference() } declaration.transformDeclarationsFlat { it.transformCallableReference() }
@@ -100,7 +128,17 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
} }
private fun replaceWithFactory(lambdaClass: IrClass): List<IrDeclaration> { private fun replaceWithFactory(lambdaClass: IrClass): List<IrDeclaration> {
return buildFactoryFunction(lambdaClass, ctorToFactoryMap).onEach { it.parent = lambdaClass.parent } val lambdaInfo = LambdaInfo(lambdaClass)
// Optimization:
// If the lambda has no context, we lift it, i.e. instead of generating a factory function that creates lambda objects,
// we generate a named free function. The usage of the lambda is then replaced with a reference to the free function.
// This allows us to avoid allocating a new object each time the lambda is created.
return if (lambdaClass.origin == CallableReferenceLowering.Companion.LAMBDA_IMPL && !lambdaInfo.isSuspendLambda && lambdaClass.fields.none()) {
liftLambda(ctorToFreeFunctionMap, lambdaInfo)
} else {
buildFactoryFunction(ctorToFactoryMap, lambdaInfo)
}.onEach { it.parent = lambdaClass.parent }
} }
} }
@@ -213,47 +251,42 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
return returnStmt.value return returnStmt.value
} }
private fun buildFactoryBody( private class LambdaInfo(val lambdaClass: IrClass) {
factoryFunction: IrSimpleFunction,
lambdaClass: IrClass,
newDeclarations: MutableList<IrDeclaration>
): IrBlockBody {
val invokeFun = lambdaClass.invokeFun!! val invokeFun = lambdaClass.invokeFun!!
val superInvokeFun = invokeFun.overriddenSymbols.first { it.owner.isSuspend == invokeFun.isSuspend }.owner val superInvokeFun = invokeFun.overriddenSymbols.first { it.owner.isSuspend == invokeFun.isSuspend }.owner
val lambdaName = Name.identifier("${lambdaClass.name.asString()}\$lambda") val isSuspendLambda = invokeFun.overriddenSymbols.any { it.owner.isSuspend }
val superClass = superInvokeFun.parentAsClass fun createOldToNewInvokeParametersMapping(lambdaDeclaration: IrSimpleFunction) =
val anyNType = context.irBuiltIns.anyNType invokeFun.valueParameters.associateBy({ it.symbol }, { lambdaDeclaration.valueParameters[it.index].symbol })
val lambdaDeclaration = context.irFactory.buildFun {
startOffset = invokeFun.startOffset
endOffset = invokeFun.endOffset
// Since box/unbox is done on declaration side in case of suspend function use the specified type
returnType = if (invokeFun.isSuspend) invokeFun.returnType else anyNType
visibility = DescriptorVisibilities.LOCAL
name = lambdaName
isSuspend = invokeFun.isSuspend
}
lambdaDeclaration.parent = factoryFunction fun lambdaInnerClasses() =
lambdaClass.declarations.filter { it is IrClass || (it is IrSimpleFunction && it.dispatchReceiverParameter == null) }
}
lambdaDeclaration.valueParameters = superInvokeFun.valueParameters.mapIndexed { id, vp -> private fun buildFactoryBody(
vp.copyTo(lambdaDeclaration, type = anyNType, name = invokeFun.valueParameters[id].name) factoryFunction: IrSimpleFunction,
} newDeclarations: MutableList<IrDeclaration>,
lambdaInfo: LambdaInfo
): IrBlockBody {
val superClass = lambdaInfo.superInvokeFun.parentAsClass
val lambdaName = Name.identifier("${lambdaInfo.lambdaClass.name.asString()}\$lambda")
val lambdaDeclaration =
createLambdaDeclaration(lambdaInfo.invokeFun, lambdaName, factoryFunction, lambdaInfo.superInvokeFun)
val statements = ArrayList<IrStatement>(4) val statements = ArrayList<IrStatement>(4)
val isSuspendLambda = invokeFun.overriddenSymbols.any { it.owner.isSuspend } val constructor = lambdaInfo.lambdaClass.declarations.firstNotNullOf { it as? IrConstructor }
val constructor = lambdaClass.declarations.firstNotNullOf { it as? IrConstructor }
if (isSuspendLambda) { if (lambdaInfo.isSuspendLambda) {
// Due to suspend lambda is a class itself it's not easy to inline it correctly and moreover I see no reason to do so // Due to suspend lambda is a class itself it's not easy to inline it correctly and moreover I see no reason to do so
val lambdaType = lambdaClass.defaultType val lambdaType = lambdaInfo.lambdaClass.defaultType
val instanceVal = JsIrBuilder.buildVar(lambdaType, factoryFunction, "i").apply { val instanceVal = JsIrBuilder.buildVar(lambdaType, factoryFunction, "i").apply {
val newCtorCall = IrConstructorCallImpl( val newCtorCall = IrConstructorCallImpl(
lambdaClass.startOffset, lambdaInfo.lambdaClass.startOffset,
lambdaClass.endOffset, lambdaInfo.lambdaClass.endOffset,
lambdaType, lambdaType,
constructor.symbol, constructor.symbol,
lambdaClass.typeParameters.size, lambdaInfo.lambdaClass.typeParameters.size,
constructor.typeParameters.size, constructor.typeParameters.size,
constructor.valueParameters.size constructor.valueParameters.size
) )
@@ -267,31 +300,25 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
statements.add(instanceVal) statements.add(instanceVal)
lambdaDeclaration.body = buildLambdaBody(instanceVal, lambdaDeclaration, invokeFun) lambdaDeclaration.body = buildLambdaBody(instanceVal, lambdaDeclaration, lambdaInfo.invokeFun)
newDeclarations.add(lambdaClass) newDeclarations.add(lambdaInfo.lambdaClass)
} else { } else {
val fieldToParameterMapping = capturedFieldsToParametersMap(constructor, factoryFunction) val fieldToParameterMapping = capturedFieldsToParametersMap(constructor, factoryFunction)
val oldToNewInvokeParametersMapping = mutableMapOf<IrValueParameterSymbol, IrValueParameterSymbol>() val oldToNewInvokeParametersMapping = lambdaInfo.createOldToNewInvokeParametersMapping(lambdaDeclaration)
invokeFun.valueParameters.forEach {
oldToNewInvokeParametersMapping[it.symbol] = lambdaDeclaration.valueParameters[it.index].symbol
}
lambdaDeclaration.body = lambdaDeclaration.body =
inlineLambdaBody(lambdaDeclaration, invokeFun, oldToNewInvokeParametersMapping, fieldToParameterMapping) inlineLambdaBody(lambdaDeclaration, lambdaInfo.invokeFun, oldToNewInvokeParametersMapping, fieldToParameterMapping)
// lambdas could contain another lambdas and local classes in so let do not lose them // lambdas can contain another lambdas and local classes in so let's not lose them
val lambdaInnerClasses = newDeclarations.addAll(lambdaInfo.lambdaInnerClasses())
lambdaClass.declarations.filter { it is IrClass || (it is IrSimpleFunction && it.dispatchReceiverParameter == null) }
newDeclarations.addAll(lambdaInnerClasses)
} }
val lambdaType = lambdaClass.superTypes.single { it.classifierOrNull === superClass.symbol } val lambdaType = lambdaInfo.lambdaClass.superTypes.single { it.classifierOrNull === superClass.symbol }
val functionExpression = lambdaClass.run { val functionExpression = lambdaInfo.lambdaClass.run {
IrFunctionExpressionImpl(startOffset, endOffset, lambdaType, lambdaDeclaration, JsStatementOrigins.CALLABLE_REFERENCE_CREATE) IrFunctionExpressionImpl(startOffset, endOffset, lambdaType, lambdaDeclaration, JsStatementOrigins.CALLABLE_REFERENCE_CREATE)
} }
val nameGetter = context.mapping.reflectedNameAccessor[lambdaClass] val nameGetter = context.mapping.reflectedNameAccessor[lambdaInfo.lambdaClass]
if (nameGetter != null || lambdaDeclaration.isSuspend) { if (nameGetter != null || lambdaDeclaration.isSuspend) {
val tmpVar = JsIrBuilder.buildVar(functionExpression.type, factoryFunction, "l", initializer = functionExpression) val tmpVar = JsIrBuilder.buildVar(functionExpression.type, factoryFunction, "l", initializer = functionExpression)
@@ -326,23 +353,48 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
statements.add(JsIrBuilder.buildReturn(factoryFunction.symbol, functionExpression, context.irBuiltIns.nothingType)) statements.add(JsIrBuilder.buildReturn(factoryFunction.symbol, functionExpression, context.irBuiltIns.nothingType))
} }
return context.irFactory.createBlockBody(lambdaClass.startOffset, lambdaClass.endOffset, statements) return context.irFactory.createBlockBody(lambdaInfo.lambdaClass.startOffset, lambdaInfo.lambdaClass.endOffset, statements)
}
private fun createLambdaDeclaration(
invokeFun: IrSimpleFunction,
lambdaName: Name,
parent: IrDeclarationParent,
superInvokeFun: IrSimpleFunction
): IrSimpleFunction {
val anyNType = context.irBuiltIns.anyNType
val lambdaDeclaration = context.irFactory.buildFun {
startOffset = invokeFun.startOffset
endOffset = invokeFun.endOffset
// Since box/unbox is done on declaration side in case of suspend function use the specified type
returnType = if (invokeFun.isSuspend) invokeFun.returnType else anyNType
visibility = DescriptorVisibilities.LOCAL
name = lambdaName
isSuspend = invokeFun.isSuspend
}
lambdaDeclaration.parent = parent
lambdaDeclaration.valueParameters = superInvokeFun.valueParameters.mapIndexed { id, vp ->
vp.copyTo(lambdaDeclaration, type = anyNType, name = invokeFun.valueParameters[id].name)
}
return lambdaDeclaration
} }
private fun buildFactoryFunction( private fun buildFactoryFunction(
lambdaClass: IrClass, ctorToFactoryMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol>,
ctorToFactoryMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol> lambdaInfo: LambdaInfo
): List<IrDeclaration> { ): List<IrDeclaration> {
val newDeclarations = mutableListOf<IrDeclaration>() val newDeclarations = mutableListOf<IrDeclaration>()
val constructor = lambdaClass.declarations.single { it is IrConstructor } as IrConstructor val constructor = lambdaInfo.lambdaClass.constructors.single()
val factoryDeclaration = context.irFactory.stageController.restrictTo(lambdaClass) { val factoryDeclaration = context.irFactory.stageController.restrictTo(lambdaInfo.lambdaClass) {
context.irFactory.buildFun { context.irFactory.buildFun {
startOffset = lambdaClass.startOffset startOffset = lambdaInfo.lambdaClass.startOffset
endOffset = lambdaClass.endOffset endOffset = lambdaInfo.lambdaClass.endOffset
visibility = lambdaClass.visibility visibility = lambdaInfo.lambdaClass.visibility
returnType = lambdaClass.defaultType returnType = lambdaInfo.lambdaClass.defaultType
name = lambdaClass.name name = lambdaInfo.lambdaClass.name
origin = JsStatementOrigins.FACTORY_ORIGIN origin = JsStatementOrigins.FACTORY_ORIGIN
} }
} }
@@ -355,7 +407,7 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
} }
} }
factoryDeclaration.body = buildFactoryBody(factoryDeclaration, lambdaClass, newDeclarations) factoryDeclaration.body = buildFactoryBody(factoryDeclaration, newDeclarations, lambdaInfo)
newDeclarations.add(factoryDeclaration) newDeclarations.add(factoryDeclaration)
ctorToFactoryMap[constructor.symbol] = factoryDeclaration.symbol ctorToFactoryMap[constructor.symbol] = factoryDeclaration.symbol
@@ -363,6 +415,38 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo
return newDeclarations return newDeclarations
} }
/**
* Replaces a contextless lambda class with a free function.
*/
private fun liftLambda(
ctorToFreeFunctionMap: MutableMap<IrConstructorSymbol, IrSimpleFunctionSymbol>,
lambdaInfo: LambdaInfo
): List<IrDeclaration> {
val constructor = lambdaInfo.lambdaClass.constructors.single()
val newDeclarations = mutableListOf<IrDeclaration>()
val freeFunctionDeclaration = createLambdaDeclaration(
lambdaInfo.invokeFun,
lambdaInfo.lambdaClass.name,
lambdaInfo.lambdaClass.parent,
lambdaInfo.superInvokeFun
)
freeFunctionDeclaration.body = inlineLambdaBody(
freeFunctionDeclaration,
lambdaInfo.invokeFun,
lambdaInfo.createOldToNewInvokeParametersMapping(freeFunctionDeclaration),
emptyMap()
)
newDeclarations.add(freeFunctionDeclaration)
// lambdas can contain another lambdas and local classes in so let's not lose them
newDeclarations.addAll(lambdaInfo.lambdaInnerClasses())
ctorToFreeFunctionMap[constructor.symbol] = freeFunctionDeclaration.symbol
return newDeclarations
}
private fun setDynamicProperty(r: IrValueSymbol, property: String, value: IrExpression): IrStatement { private fun setDynamicProperty(r: IrValueSymbol, property: String, value: IrExpression): IrStatement {
return IrDynamicOperatorExpressionImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.unitType, IrDynamicOperator.EQ).apply { return IrDynamicOperatorExpressionImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.unitType, IrDynamicOperator.EQ).apply {
@@ -2,7 +2,7 @@
// CHECK_CASES_COUNT: function=bar1_u51tkt$ count=3 TARGET_BACKENDS=JS // CHECK_CASES_COUNT: function=bar1_u51tkt$ count=3 TARGET_BACKENDS=JS
// CHECK_IF_COUNT: function=bar1_u51tkt$ count=0 TARGET_BACKENDS=JS // CHECK_IF_COUNT: function=bar1_u51tkt$ count=0 TARGET_BACKENDS=JS
// CHECK_CASES_COUNT: function=A$bar2$lambda count=3 TARGET_BACKENDS=JS // CHECK_CASES_COUNT: function=A$bar2$lambda count=3 TARGET_BACKENDS=JS
// CHECK_CASES_COUNT: function=A$bar2$lambda count=0 IGNORED_BACKENDS=JS // CHECK_CASES_COUNT: function=A$bar2$lambda count=4 IGNORED_BACKENDS=JS
// CHECK_IF_COUNT: function=A$bar2$lambda count=0 // CHECK_IF_COUNT: function=A$bar2$lambda count=0
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -9,7 +9,7 @@ inline fun inlineFun(crossinline inlineLambda: () -> String = { "OK" }, noinline
// FILE: 2.kt // FILE: 2.kt
// CHECK_CALLED_IN_SCOPE: function=inlineFun$lambda_0 scope=box TARGET_BACKENDS=JS // CHECK_CALLED_IN_SCOPE: function=inlineFun$lambda_0 scope=box TARGET_BACKENDS=JS
// CHECK_CALLED_IN_SCOPE: function=box$lambda scope=box IGNORED_BACKENDS=JS // HAS_NO_CAPTURED_VARS: function=box except=box$lambda IGNORED_BACKENDS=JS
import test.* import test.*
fun box(): String { fun box(): String {
@@ -10,7 +10,7 @@ fun call(lambda: () -> String ) = lambda()
// FILE: 2.kt // FILE: 2.kt
// CHECK_CALLED_IN_SCOPE: function=inlineFun$lambda scope=box TARGET_BACKENDS=JS // CHECK_CALLED_IN_SCOPE: function=inlineFun$lambda scope=box TARGET_BACKENDS=JS
// CHECK_CALLED_IN_SCOPE: function=box$lambda scope=box IGNORED_BACKENDS=JS // HAS_NO_CAPTURED_VARS: function=box except=box$lambda;call IGNORED_BACKENDS=JS
// CHECK_CALLED_IN_SCOPE: function=call scope=box // CHECK_CALLED_IN_SCOPE: function=call scope=box
import test.* import test.*
+4 -2
View File
@@ -1,7 +1,9 @@
// EXPECTED_REACHABLE_NODES: 1284 // EXPECTED_REACHABLE_NODES: 1284
package foo package foo
// CHECK_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda // CHECK_FUNCTION_EXISTS: multiplyBy2$lambda
// CHECK_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda TARGET_BACKENDS=JS
// HAS_NO_CAPTURED_VARS: function=multiplyBy2 except=multiplyBy2$lambda
// CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda_0 // CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda_0
// CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=run // CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=run
@@ -19,4 +21,4 @@ fun box(): String {
assertEquals(8, multiplyBy2(4)) assertEquals(8, multiplyBy2(4))
return "OK" return "OK"
} }
+4 -2
View File
@@ -1,7 +1,9 @@
// EXPECTED_REACHABLE_NODES: 1284 // EXPECTED_REACHABLE_NODES: 1284
package foo package foo
// CHECK_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda // CHECK_FUNCTION_EXISTS: multiplyBy2$lambda
// CHECK_CALLED_IN_SCOPE: scope=multiplyBy2 function=multiplyBy2$lambda TARGET_BACKENDS=JS
// HAS_NO_CAPTURED_VARS: function=multiplyBy2 except=multiplyBy2$lambda
// CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=run // CHECK_NOT_CALLED_IN_SCOPE: scope=multiplyBy2 function=run
internal inline fun <T> run(noinline func: (T) -> T, arg: T): T { internal inline fun <T> run(noinline func: (T) -> T, arg: T): T {
@@ -18,4 +20,4 @@ fun box(): String {
assertEquals(8, multiplyBy2(4)) assertEquals(8, multiplyBy2(4))
return "OK" return "OK"
} }