From e1520c61da87213cdf733a70ca06684475c054db Mon Sep 17 00:00:00 2001 From: pyos Date: Thu, 16 Sep 2021 13:14:27 +0200 Subject: [PATCH] IR: use parameters for captures in field/instance initializers val y = 1 object { val x = y } -> class XKt$1(`$y`: Int) { val x: Int = `$y` } Note that `$y` is not stored in a field because it's not used outside the primary constructor. One exception is captured inline parameters on the JVM backend, as the bytecode inliner uses field assignment instructions (setfield) to locate them; removing the field is thus not possible. --- .../common/lower/LocalDeclarationsLowering.kt | 76 ++++++++++++++----- .../jetbrains/kotlin/backend/jvm/JvmLower.kt | 3 +- .../localClassWithCapturedParams_ir.txt | 25 ++++++ .../constructorOnly.kt | 4 +- 4 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 compiler/testData/codegen/bytecodeListing/annotations/localClassWithCapturedParams_ir.txt diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/LocalDeclarationsLowering.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/LocalDeclarationsLowering.kt index daf11aee8b2..55746e2eea8 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/LocalDeclarationsLowering.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/LocalDeclarationsLowering.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.common.CommonBackendContext import org.jetbrains.kotlin.backend.common.BodyAndScriptBodyLoweringPass import org.jetbrains.kotlin.backend.common.descriptors.synthesizedString import org.jetbrains.kotlin.backend.common.ir.* +import org.jetbrains.kotlin.backend.common.lower.inline.isInlineParameter import org.jetbrains.kotlin.backend.common.runOnFilePostfix import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality @@ -79,6 +80,7 @@ class LocalDeclarationsLowering( val localNameProvider: LocalNameProvider = LocalNameProvider.DEFAULT, val visibilityPolicy: VisibilityPolicy = VisibilityPolicy.DEFAULT, val suggestUniqueNames: Boolean = true, // When `true` appends a `-#index` suffix to lifted declaration names + val forceFieldsForInlineCaptures: Boolean = false // See `LocalClassContext` ) : BodyAndScriptBodyLoweringPass { @@ -163,7 +165,11 @@ class LocalDeclarationsLowering( override lateinit var transformedDeclaration: IrConstructor } - private class LocalClassContext(val declaration: IrClass, val inInlineFunctionScope: Boolean) : LocalContext() { + private inner class LocalClassContext( + val declaration: IrClass, + val inInlineFunctionScope: Boolean, + val constructorContext: LocalContext? + ) : LocalContext() { lateinit var closure: Closure // NOTE: This map is iterated over in `rewriteClassMembers` and we're relying on @@ -171,6 +177,19 @@ class LocalDeclarationsLowering( val capturedValueToField: MutableMap> = mutableMapOf() override fun irGet(startOffset: Int, endOffset: Int, valueDeclaration: IrValueDeclaration): IrExpression? { + // On the JVM backend, `AnonymousObjectTransformer` in the bytecode inliner uses field assignment + // instructions to find captured parameters. Most of the time this is unimportant, as captured + // parameters are effectively indistinguishable from normal ones; the exception is ones that can + // take an inline lambda value, as the parameter needs to be replaced with the lambda's capture. + // Thus we cannot erase the field - the inliner won't handle the inline lambda without it. + // (Most of the time this is inconsequential - if the object really is instantiated with an inline + // lambda, the field will be erased completely regardless of whether it's used.) + if (!forceFieldsForInlineCaptures || !valueDeclaration.isInlineDeclaration()) { + // We're in the initializer scope, which will be moved to a primary constructor later. + // Thus we can directly use that constructor's context and read from a parameter instead of a field. + constructorContext?.irGet(startOffset, endOffset, valueDeclaration)?.let { return it } + } + val field = capturedValueToField[valueDeclaration]?.value ?: return null val receiver = declaration.thisReceiver!! @@ -179,13 +198,16 @@ class LocalDeclarationsLowering( receiver = IrGetValueImpl(startOffset, endOffset, receiver.type, receiver.symbol) ) } + + private fun IrValueDeclaration.isInlineDeclaration() = + this is IrValueParameter && parent.let { it is IrFunction && it.isInline } && isInlineParameter() } - private class LocalClassMemberContext(val member: IrFunction, val classContext: LocalClassContext) : LocalContext() { + private class LocalClassMemberContext(val member: IrDeclaration, val classContext: LocalClassContext) : LocalContext() { override fun irGet(startOffset: Int, endOffset: Int, valueDeclaration: IrValueDeclaration): IrExpression? { val field = classContext.capturedValueToField[valueDeclaration]?.value ?: return null - - val receiver = member.dispatchReceiverParameter + // This lowering does not process accesses to outer `this`. + val receiver = (if (member is IrFunction) member.dispatchReceiverParameter else classContext.declaration.thisReceiver) ?: error("No dispatch receiver parameter for ${member.render()}") return IrGetFieldImpl( startOffset, endOffset, field.symbol, field.type, @@ -258,27 +280,26 @@ class LocalDeclarationsLowering( // Both accessors extracted as closures. declaration.delegate.transformStatement(this) - override fun visitClass(declaration: IrClass) = if (declaration in localClasses) { - localClasses[declaration]!!.declaration - } else { - super.visitClass(declaration) - } + override fun visitClass(declaration: IrClass) = + localClasses[declaration]?.declaration + ?: visitMember(declaration) + ?: super.visitClass(declaration) - override fun visitFunction(declaration: IrFunction): IrStatement { - return if (declaration in localFunctions) { + override fun visitFunction(declaration: IrFunction): IrStatement = + if (declaration in localFunctions) { // Replace local function definition with an empty composite. IrCompositeImpl(declaration.startOffset, declaration.endOffset, context.irBuiltIns.unitType) } else { - if (localContext is LocalClassContext && declaration.parent == localContext.declaration) { - declaration.apply { - val classMemberLocalContext = LocalClassMemberContext(declaration, localContext) - transformChildrenVoid(FunctionBodiesRewriter(classMemberLocalContext)) - } - } else { - super.visitFunction(declaration) - } + visitMember(declaration) ?: super.visitFunction(declaration) + } + + private fun visitMember(declaration: IrDeclaration): IrStatement? = + if (localContext is LocalClassContext && declaration.parent == localContext.declaration) { + val classMemberLocalContext = LocalClassMemberContext(declaration, localContext) + declaration.apply { transformChildrenVoid(FunctionBodiesRewriter(classMemberLocalContext)) } + } else { + null } - } override fun visitConstructor(declaration: IrConstructor): IrStatement { // Body is transformed separately. See loop over constructors in rewriteDeclarations(). @@ -469,6 +490,8 @@ class LocalDeclarationsLowering( if (isScriptMode && constructors.isEmpty()) return + // NOTE: if running before InitializersLowering, we can instead look for constructors that have + // IrInstanceInitializerCall. However, Native runs these two lowerings in opposite order. val constructorsCallingSuper = constructors .asSequence() .map { localClassConstructors[it]!! } @@ -953,7 +976,18 @@ class LocalDeclarationsLowering( // LDL doesn't work on the enums because local enums are not allowed, so skipping them in scripts too if (isScriptMode && declaration.kind == ClassKind.ENUM_CLASS) return - localClasses[declaration] = LocalClassContext(declaration, data.inInlineFunctionScope) + // If there are many non-delegating constructors, each copy of the initializer requires different remapping: + // class C { + // constructor() {} + // constructor(x: Int) {} + // val x = y // which constructor's parameter? + // } + // TODO: this should ideally run after initializers are added to constructors, but that'd place + // other restrictions on IR (e.g. after the initializers are moved you can no longer create fields + // with initializers) which makes that hard to implement. + val constructorContext = declaration.constructors.mapNotNull { localClassConstructors[it] } + .singleOrNull { it.declaration.callsSuper(context.irBuiltIns) } + localClasses[declaration] = LocalClassContext(declaration, data.inInlineFunctionScope, constructorContext) } private val Data.inInlineFunctionScope: Boolean diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt index e43d1e54c46..61c4f26ec9f 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt @@ -141,7 +141,8 @@ internal val localDeclarationsPhase = makeIrFilePhase( private fun scopedVisibility(inInlineFunctionScope: Boolean): DescriptorVisibility = if (inInlineFunctionScope) DescriptorVisibilities.PUBLIC else JavaDescriptorVisibilities.PACKAGE_VISIBILITY - } + }, + forceFieldsForInlineCaptures = true ) }, name = "JvmLocalDeclarations", diff --git a/compiler/testData/codegen/bytecodeListing/annotations/localClassWithCapturedParams_ir.txt b/compiler/testData/codegen/bytecodeListing/annotations/localClassWithCapturedParams_ir.txt new file mode 100644 index 00000000000..8eeee8fa6a4 --- /dev/null +++ b/compiler/testData/codegen/bytecodeListing/annotations/localClassWithCapturedParams_ir.txt @@ -0,0 +1,25 @@ +@kotlin.Metadata +public final class LocalClassWithCapturedParamsKt$localCaptured$A { + // source: 'localClassWithCapturedParams.kt' + enclosing method LocalClassWithCapturedParamsKt.localCaptured()Ljava/lang/Object; + private final field x: int + private final @org.jetbrains.annotations.NotNull field z: java.lang.String + inner (local) class LocalClassWithCapturedParamsKt$localCaptured$A A + public method (p0: int, @Simple(value="K") @org.jetbrains.annotations.NotNull p1: java.lang.String): void + public final method getX(): int + public final @org.jetbrains.annotations.NotNull method getZ(): java.lang.String +} + +@kotlin.Metadata +public final class LocalClassWithCapturedParamsKt { + // source: 'localClassWithCapturedParams.kt' + inner (local) class LocalClassWithCapturedParamsKt$localCaptured$A A + public final static @org.jetbrains.annotations.NotNull method localCaptured(): java.lang.Object +} + +@java.lang.annotation.Retention(value=RUNTIME) +@kotlin.Metadata +public annotation class Simple { + // source: 'localClassWithCapturedParams.kt' + public abstract method value(): java.lang.String +} diff --git a/compiler/testData/codegen/bytecodeText/fieldsForCapturedValues/constructorOnly.kt b/compiler/testData/codegen/bytecodeText/fieldsForCapturedValues/constructorOnly.kt index 30a464ab4de..eb32f697ef9 100644 --- a/compiler/testData/codegen/bytecodeText/fieldsForCapturedValues/constructorOnly.kt +++ b/compiler/testData/codegen/bytecodeText/fieldsForCapturedValues/constructorOnly.kt @@ -2,8 +2,8 @@ open class Base(parameter: String) fun foo(captured: String) { object : Base(captured) { - // val x = captured - // init { println(captured) } + val x = captured + init { println(captured) } } }