From c9461a382744a8651477e3e94e16fe23fc072c74 Mon Sep 17 00:00:00 2001 From: Marco Pennekamp Date: Fri, 13 Jan 2023 19:46:52 +0100 Subject: [PATCH] [JVM IR] KTIJ-24206 Add an option to stub orphaned `expect` symbols - The cause for KTIJ-24206 is that the `expect` function's parent is an `IrFile` instead of an `IrClass`. This is because `ExpectDeclarationsRemoveLowering` removes `expect` declarations before `FileClassLowering` can replace `IrFile` parents. - That behavior is normally okay, but breaks down when an `expect` declaration has no associated `actual` declaration. In such cases, `ExpectDeclarationsRemoveLowering` doesn't replace `expect` symbols in expressions with their corresponding `actual` symbols, as it normally would. - The solution fills in `ExpectDeclarationsRemoveLowering`'s behavior by replacing `expect` symbols for which no `actual` symbols exist with stubs. See `stubOrphanedExpectSymbols`. - To not mess with the lowerings, `stubOrphanedExpectSymbols` is invoked during IR generation. It uses the same `ExpectSymbolTransformer` as `ExpectDeclarationRemover`. ^KTIJ-24206 fixed --- .../common/ir/ExpectSymbolTransformer.kt | 153 ++++++++++++++ .../common/lower/ExpectDeclarationRemover.kt | 187 +++++------------- .../kotlin/backend/jvm/JvmIrCodegenFactory.kt | 18 +- .../kotlin/backend/jvm/OrphanedExpectUtils.kt | 132 +++++++++++++ 4 files changed, 345 insertions(+), 145 deletions(-) create mode 100644 compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/ir/ExpectSymbolTransformer.kt create mode 100644 compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/OrphanedExpectUtils.kt diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/ir/ExpectSymbolTransformer.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/ir/ExpectSymbolTransformer.kt new file mode 100644 index 00000000000..6685bd71a66 --- /dev/null +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/ir/ExpectSymbolTransformer.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.common.ir + +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.* +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol +import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.util.copyTypeAndValueArgumentsFrom +import org.jetbrains.kotlin.ir.util.irCall +import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid + +/** + * [ExpectSymbolTransformer] replaces `expect` symbols in expressions with `actual` symbols. An `actual` symbol must be provided by + * overriding [getActualClass], [getActualProperty], [getActualConstructor], and [getActualFunction]. + */ +@OptIn(ObsoleteDescriptorBasedAPI::class) +abstract class ExpectSymbolTransformer : IrElementTransformerVoid() { + + protected abstract fun getActualClass(descriptor: ClassDescriptor): IrClassSymbol? + + protected data class ActualPropertyResult( + val propertySymbol: IrPropertySymbol, + val getterSymbol: IrSimpleFunctionSymbol?, + val setterSymbol: IrSimpleFunctionSymbol?, + ) + + protected abstract fun getActualProperty(descriptor: PropertyDescriptor): ActualPropertyResult? + + protected abstract fun getActualConstructor(descriptor: ClassConstructorDescriptor): IrConstructorSymbol? + + protected abstract fun getActualFunction(descriptor: FunctionDescriptor): IrSimpleFunctionSymbol? + + override fun visitElement(element: IrElement): IrElement { + element.transformChildrenVoid() + return element + } + + override fun visitConstructorCall(expression: IrConstructorCall): IrExpression { + val nExpression = super.visitConstructorCall(expression) as IrConstructorCall + if (!nExpression.symbol.owner.isExpect) return nExpression + + val newCallee = getActualConstructor(nExpression.symbol.descriptor) ?: return nExpression + with(nExpression) { + return IrConstructorCallImpl( + startOffset, endOffset, type, newCallee, typeArgumentsCount, constructorTypeArgumentsCount, valueArgumentsCount, origin + ).also { + it.attributeOwnerId = attributeOwnerId + it.copyTypeAndValueArgumentsFrom(nExpression) + } + } + } + + override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall): IrExpression { + val nExpression = super.visitDelegatingConstructorCall(expression) as IrDelegatingConstructorCall + if (!nExpression.symbol.owner.isExpect) return nExpression + + val newCallee = getActualConstructor(nExpression.symbol.descriptor) ?: return nExpression + with(nExpression) { + return IrDelegatingConstructorCallImpl( + startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount + ).also { + it.attributeOwnerId = attributeOwnerId + it.copyTypeAndValueArgumentsFrom(nExpression) + } + } + } + + override fun visitEnumConstructorCall(expression: IrEnumConstructorCall): IrExpression { + val nExpression = super.visitEnumConstructorCall(expression) as IrEnumConstructorCall + if (!nExpression.symbol.owner.isExpect) return nExpression + + val newCallee = getActualConstructor(nExpression.symbol.descriptor) ?: return nExpression + with(nExpression) { + return IrEnumConstructorCallImpl( + startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount + ).also { + it.attributeOwnerId = attributeOwnerId + it.copyTypeAndValueArgumentsFrom(nExpression) + } + } + } + + override fun visitCall(expression: IrCall): IrExpression { + val nExpression = super.visitCall(expression) as IrCall + if (!nExpression.symbol.owner.isExpect) return nExpression + + val newCallee = getActualFunction(nExpression.symbol.descriptor) ?: return nExpression + return irCall(nExpression, newCallee).also { + it.attributeOwnerId = nExpression.attributeOwnerId + } + } + + override fun visitPropertyReference(expression: IrPropertyReference): IrExpression { + val nExpression = super.visitPropertyReference(expression) as IrPropertyReference + if (!nExpression.symbol.owner.isExpect) return nExpression + + val (newSymbol, newGetter, newSetter) = getActualProperty(nExpression.symbol.descriptor) ?: return nExpression + with(nExpression) { + return IrPropertyReferenceImpl( + startOffset, endOffset, type, + newSymbol, typeArgumentsCount, + field, newGetter, newSetter, + origin + ).also { + it.attributeOwnerId = attributeOwnerId + copyTypeArgumentsFrom(nExpression) + it.dispatchReceiver = dispatchReceiver + it.extensionReceiver = extensionReceiver + } + } + } + + override fun visitFunctionReference(expression: IrFunctionReference): IrExpression { + val nExpression = super.visitFunctionReference(expression) as IrFunctionReference + if (!nExpression.symbol.owner.isExpect) return nExpression + + val newCallee = getActualFunction(nExpression.symbol.descriptor) ?: return nExpression + with(nExpression) { + return IrFunctionReferenceImpl( + startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount, reflectionTarget, origin + ).also { + it.attributeOwnerId = attributeOwnerId + it.copyTypeArgumentsFrom(nExpression) + it.dispatchReceiver = dispatchReceiver + it.extensionReceiver = extensionReceiver + } + } + } + + override fun visitClassReference(expression: IrClassReference): IrExpression { + val nExpression = super.visitClassReference(expression) as IrClassReference + val oldSymbol = nExpression.symbol as? IrClassSymbol ?: return nExpression + if (!oldSymbol.owner.isExpect) return nExpression + + val newSymbol = getActualClass(oldSymbol.descriptor) ?: return nExpression + with(nExpression) { + return IrClassReferenceImpl(startOffset, endOffset, type, newSymbol, classType) + } + } + +} diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/ExpectDeclarationRemover.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/ExpectDeclarationRemover.kt index 43231187b4e..e71a22ad04a 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/ExpectDeclarationRemover.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/ExpectDeclarationRemover.kt @@ -7,26 +7,28 @@ package org.jetbrains.kotlin.backend.common.lower import org.jetbrains.kotlin.backend.common.BackendContext import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.ir.ExpectSymbolTransformer import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI import org.jetbrains.kotlin.ir.declarations.* -import org.jetbrains.kotlin.ir.expressions.* -import org.jetbrains.kotlin.ir.expressions.impl.* -import org.jetbrains.kotlin.ir.symbols.IrClassSymbol -import org.jetbrains.kotlin.ir.symbols.IrValueParameterSymbol -import org.jetbrains.kotlin.ir.symbols.IrValueSymbol +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrGetValue +import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl +import org.jetbrains.kotlin.ir.symbols.* import org.jetbrains.kotlin.ir.types.extractTypeParameters import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid import org.jetbrains.kotlin.resolve.descriptorUtil.module -import org.jetbrains.kotlin.resolve.multiplatform.* +import org.jetbrains.kotlin.resolve.multiplatform.OptionalAnnotationUtil +import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleActualsForExpected +import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleExpectsForActual +import kotlin.collections.set // `doRemove` means should expect-declaration be removed from IR @OptIn(ObsoleteDescriptorBasedAPI::class) -class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private val doRemove: Boolean) - : IrElementTransformerVoid(), FileLoweringPass { +open class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private val doRemove: Boolean) + : ExpectSymbolTransformer(), FileLoweringPass { constructor(context: BackendContext) : this(context.ir.symbols.externalSymbolTable, true) @@ -36,14 +38,9 @@ class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private va visitFile(irFile) } - override fun visitElement(element: IrElement): IrElement { - element.transformChildrenVoid() - return element - } - override fun visitFile(declaration: IrFile): IrFile { - declaration.declarations.removeAll { - shouldRemoveTopLevelDeclaration(it) + if (doRemove) { + declaration.declarations.removeAll { shouldRemoveTopLevelDeclaration(it) } } return super.visitFile(declaration) } @@ -53,127 +50,7 @@ class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private va return super.visitValueParameter(declaration) } - override fun visitConstructorCall(expression: IrConstructorCall): IrExpression { - val nExpression = super.visitConstructorCall(expression) as IrConstructorCall - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newCallee = symbolTable.referenceConstructor( - nExpression.symbol.descriptor.findActualForExpect() as? ClassConstructorDescriptor ?: return nExpression - ) - with(nExpression) { - return IrConstructorCallImpl( - startOffset, endOffset, type, newCallee, typeArgumentsCount, constructorTypeArgumentsCount, valueArgumentsCount, origin - ).also { - it.attributeOwnerId = attributeOwnerId - it.copyTypeAndValueArgumentsFrom(nExpression) - } - } - } - - override fun visitDelegatingConstructorCall(expression: IrDelegatingConstructorCall): IrExpression { - val nExpression = super.visitDelegatingConstructorCall(expression) as IrDelegatingConstructorCall - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newCallee = symbolTable.referenceConstructor( - nExpression.symbol.descriptor.findActualForExpect() as? ClassConstructorDescriptor ?: return nExpression - ) - with(nExpression) { - return IrDelegatingConstructorCallImpl( - startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount - ).also { - it.attributeOwnerId = attributeOwnerId - it.copyTypeAndValueArgumentsFrom(nExpression) - } - } - } - - override fun visitEnumConstructorCall(expression: IrEnumConstructorCall): IrExpression { - val nExpression = super.visitEnumConstructorCall(expression) as IrEnumConstructorCall - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newCallee = symbolTable.referenceConstructor( - nExpression.symbol.descriptor.findActualForExpect() as? ClassConstructorDescriptor ?: return nExpression - ) - with(nExpression) { - return IrEnumConstructorCallImpl( - startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount - ).also { - it.attributeOwnerId = attributeOwnerId - it.copyTypeAndValueArgumentsFrom(nExpression) - } - } - } - - override fun visitCall(expression: IrCall): IrExpression { - val nExpression = super.visitCall(expression) as IrCall - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newCallee = symbolTable.referenceSimpleFunction( - nExpression.symbol.descriptor.findActualForExpect() as? FunctionDescriptor ?: return nExpression - ) - return irCall(nExpression, newCallee).also { - it.attributeOwnerId = nExpression.attributeOwnerId - } - } - - override fun visitPropertyReference(expression: IrPropertyReference): IrExpression { - val nExpression = super.visitPropertyReference(expression) as IrPropertyReference - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newSymbol = symbolTable.referenceProperty( - nExpression.symbol.descriptor.findActualForExpect() as? PropertyDescriptor ?: return nExpression - ) - val newGetter = newSymbol.descriptor.getter?.let { symbolTable.referenceSimpleFunction(it) } - val newSetter = newSymbol.descriptor.setter?.let { symbolTable.referenceSimpleFunction(it) } - with(nExpression) { - return IrPropertyReferenceImpl( - startOffset, endOffset, type, - newSymbol, typeArgumentsCount, - field, newGetter, newSetter, - origin - ).also { - it.attributeOwnerId = attributeOwnerId - copyTypeArgumentsFrom(nExpression) - it.dispatchReceiver = dispatchReceiver - it.extensionReceiver = extensionReceiver - } - } - } - - override fun visitFunctionReference(expression: IrFunctionReference): IrExpression { - val nExpression = super.visitFunctionReference(expression) as IrFunctionReference - if (!nExpression.symbol.owner.isExpect) return nExpression - - val newCallee = symbolTable.referenceSimpleFunction( - nExpression.symbol.descriptor.findActualForExpect() as? FunctionDescriptor ?: return nExpression - ) - with(nExpression) { - return IrFunctionReferenceImpl( - startOffset, endOffset, type, newCallee, typeArgumentsCount, valueArgumentsCount, reflectionTarget, origin - ).also { - it.attributeOwnerId = attributeOwnerId - it.copyTypeArgumentsFrom(nExpression) - it.dispatchReceiver = dispatchReceiver - it.extensionReceiver = extensionReceiver - } - } - } - - override fun visitClassReference(expression: IrClassReference): IrExpression { - val nExpression = super.visitClassReference(expression) as IrClassReference - val oldSymbol = nExpression.symbol as? IrClassSymbol ?: return nExpression - if (!oldSymbol.owner.isExpect) return nExpression - - val newSymbol = symbolTable.referenceClass( - oldSymbol.descriptor.findActualForExpect() as? ClassDescriptor ?: return nExpression - ) - with(nExpression) { - return IrClassReferenceImpl(startOffset, endOffset, type, newSymbol, classType) - } - } - fun transformFlat(declaration: IrDeclaration): List? { - if (declaration.isTopLevelDeclaration && shouldRemoveTopLevelDeclaration(declaration)) { return emptyList() } @@ -185,6 +62,38 @@ class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private va return null } + override fun getActualClass(descriptor: ClassDescriptor): IrClassSymbol? { + return symbolTable.referenceClass( + descriptor.findActualForExpect() as? ClassDescriptor ?: return null + ) + } + + override fun getActualProperty(descriptor: PropertyDescriptor): ActualPropertyResult? { + val newSymbol = symbolTable.referenceProperty( + descriptor.findActualForExpect() as? PropertyDescriptor ?: return null + ) + val newGetter = newSymbol.descriptor.getter?.let { symbolTable.referenceSimpleFunction(it) } + val newSetter = newSymbol.descriptor.setter?.let { symbolTable.referenceSimpleFunction(it) } + return ActualPropertyResult(newSymbol, newGetter, newSetter) + } + + override fun getActualConstructor(descriptor: ClassConstructorDescriptor): IrConstructorSymbol? { + return symbolTable.referenceConstructor( + descriptor.findActualForExpect() as? ClassConstructorDescriptor ?: return null + ) + } + + override fun getActualFunction(descriptor: FunctionDescriptor): IrSimpleFunctionSymbol? { + return symbolTable.referenceSimpleFunction( + descriptor.findActualForExpect() as? FunctionDescriptor ?: return null + ) + } + + private fun MemberDescriptor.findActualForExpect(): MemberDescriptor? { + if (!isExpect) error(this) + return findCompatibleActualsForExpected(module).singleOrNull() + } + private fun shouldRemoveTopLevelDeclaration(declaration: IrDeclaration): Boolean { return doRemove && when (declaration) { is IrClass -> declaration.isExpect @@ -257,12 +166,6 @@ class ExpectDeclarationRemover(val symbolTable: ReferenceSymbolTable, private va } } - private fun MemberDescriptor.findActualForExpect(): MemberDescriptor? { - if (!isExpect) error(this) - return findCompatibleActualsForExpected(module).singleOrNull() - - } - private fun MemberDescriptor.findExpectForActual(): MemberDescriptor? { if (!isActual) error(this) return findCompatibleExpectsForActual().singleOrNull() diff --git a/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/JvmIrCodegenFactory.kt b/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/JvmIrCodegenFactory.kt index d5046ba13b2..b5cf27224d8 100644 --- a/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/JvmIrCodegenFactory.kt +++ b/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/JvmIrCodegenFactory.kt @@ -60,7 +60,7 @@ open class JvmIrCodegenFactory( private val externalSymbolTable: SymbolTable? = null, private val jvmGeneratorExtensions: JvmGeneratorExtensionsImpl = JvmGeneratorExtensionsImpl(configuration), private val evaluatorFragmentInfoForPsi2Ir: EvaluatorFragmentInfo? = null, - private val shouldStubAndNotLinkUnboundSymbols: Boolean = false, + private val stubSettings: StubSettings = StubSettings(), ) : CodegenFactory { @IDEAPluginsCompatibilityAPI(IDEAPlatforms._221, message = "Please migrate to the other constructor", plugins = "Android Studio") @@ -81,7 +81,15 @@ open class JvmIrCodegenFactory( externalSymbolTable, jvmGeneratorExtensions, evaluatorFragmentInfoForPsi2Ir, - shouldStubAndNotLinkUnboundSymbols + StubSettings(shouldStubAndNotLinkUnboundSymbols = shouldStubAndNotLinkUnboundSymbols), + ) + + /** + * @param shouldStubOrphanedExpectSymbols See [stubOrphanedExpectSymbols]. + */ + data class StubSettings( + val shouldStubAndNotLinkUnboundSymbols: Boolean = false, + val shouldStubOrphanedExpectSymbols: Boolean = false, ) data class JvmIrBackendInput( @@ -208,7 +216,7 @@ open class JvmIrCodegenFactory( } - val irProviders = if (shouldStubAndNotLinkUnboundSymbols) { + val irProviders = if (stubSettings.shouldStubAndNotLinkUnboundSymbols) { listOf(stubGenerator) } else { val stubGeneratorForMissingClasses = DeclarationStubGeneratorForNotFoundClasses(stubGenerator) @@ -232,6 +240,10 @@ open class JvmIrCodegenFactory( // We need to compile all files we reference in Klibs irModuleFragment.files.addAll(dependencies.flatMap { it.files }) + if (stubSettings.shouldStubOrphanedExpectSymbols) { + irModuleFragment.stubOrphanedExpectSymbols(stubGenerator) + } + if (!input.configuration.getBoolean(JVMConfigurationKeys.DO_NOT_CLEAR_BINDING_CONTEXT)) { val originalBindingContext = input.bindingContext as? CleanableBindingContext ?: error("BindingContext should be cleanable in JVM IR to avoid leaking memory: ${input.bindingContext}") diff --git a/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/OrphanedExpectUtils.kt b/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/OrphanedExpectUtils.kt new file mode 100644 index 00000000000..45607eaede9 --- /dev/null +++ b/compiler/ir/backend.jvm/entrypoint/src/org/jetbrains/kotlin/backend/jvm/OrphanedExpectUtils.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.jvm + +import org.jetbrains.kotlin.backend.common.ir.ExpectSymbolTransformer +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrDeclaration +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.util.DeclarationStubGenerator +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleActualsForExpected + +/** + * Replaces `expect` symbols for which no `actual` counterpart exists with an `actual` stub in all files of the [IrModuleFragment]. The + * implementation keeps track of generated stubs and only generates a single stub for each unique `expect` symbol. + * + * [stubOrphanedExpectSymbols] is used by the IDE bytecode tool window to allow compiling source files with `expect` declarations for which + * the compiled module has no `actual` declaration. (The `actual` declaration would be defined in a module dependent on the compiled + * module, but choosing this module is non-trivial due to possibly multiple implementations of the same `expect` symbol. In addition, when + * generating bytecode for a single source file, the number of source files to compile should be kept low. Stubbing helps with that.) + */ +internal fun IrModuleFragment.stubOrphanedExpectSymbols(stubGenerator: DeclarationStubGenerator) { + val transformer = StubOrphanedExpectSymbolTransformer(stubGenerator) + files.forEach(transformer::visitFile) +} + +private class StubOrphanedExpectSymbolTransformer(val stubGenerator: DeclarationStubGenerator) : ExpectSymbolTransformer() { + + private val stubbedClasses = mutableMapOf() + private val stubbedProperties = mutableMapOf() + private val stubbedConstructors = mutableMapOf() + private val stubbedFunctions = mutableMapOf() + + override fun getActualClass(descriptor: ClassDescriptor): IrClassSymbol? { + if (!descriptor.isOrphanedExpect()) return null + + return stubbedClasses.getOrPut(descriptor) { + stubGenerator.generateClassStub(FakeActualClassDescriptor(descriptor)).symbol + } + } + + override fun getActualProperty(descriptor: PropertyDescriptor): ActualPropertyResult? { + if (!descriptor.isOrphanedExpect()) return null + + return stubbedProperties.getOrPut(descriptor) { + val irProperty = + stubGenerator.generatePropertyStub(FakeActualPropertyDescriptor(descriptor)).apply { ensureClassParent(descriptor) } + val irGetter = descriptor.getter?.let(::getActualFunction) + val irSetter = descriptor.setter?.let(::getActualFunction) + ActualPropertyResult(irProperty.symbol, irGetter, irSetter) + } + } + + override fun getActualConstructor(descriptor: ClassConstructorDescriptor): IrConstructorSymbol? { + if (!descriptor.isOrphanedExpect()) return null + + return stubbedConstructors.getOrPut(descriptor) { + stubGenerator.generateConstructorStub(FakeActualClassConstructorDescriptor(descriptor)).symbol + } + } + + override fun getActualFunction(descriptor: FunctionDescriptor): IrSimpleFunctionSymbol? { + if (!descriptor.isOrphanedExpect()) return null + + return stubbedFunctions.getOrPut(descriptor) { + stubGenerator + .generateFunctionStub(FakeActualFunctionDescriptor(descriptor), createPropertyIfNeeded = false) + .apply { ensureClassParent(descriptor) } + .symbol + } + } + + /** + * If an `actual` symbol exists, we shouldn't stub the `expect` symbol. This will be performed by + * [org.jetbrains.kotlin.backend.common.lower.ExpectDeclarationsRemoveLowering] during lowering. + */ + private fun MemberDescriptor.isOrphanedExpect(): Boolean = findCompatibleActualsForExpected(module).isEmpty() + + /** + * [descriptor] should be the original descriptor, because the copied `actual` descriptor has no source. + */ + private fun IrDeclaration.ensureClassParent(descriptor: MemberDescriptor) { + if (parent !is IrClass) { + parent = stubGenerator.generateOrGetFacadeClass(descriptor) ?: return + } + } + +} + +private class FakeActualClassDescriptor(original: ClassDescriptor) : ClassDescriptor by original { + override fun isActual(): Boolean = true + override fun isExpect(): Boolean = false + + override fun getSource(): SourceElement = SourceElement.NO_SOURCE + override fun getOriginal(): ClassDescriptor = this +} + +private class FakeActualPropertyDescriptor(original: PropertyDescriptor) : PropertyDescriptor by original { + override fun isActual(): Boolean = true + override fun isExpect(): Boolean = false + + override fun getSource(): SourceElement = SourceElement.NO_SOURCE + override fun getOriginal(): PropertyDescriptor = this +} + +private class FakeActualClassConstructorDescriptor(original: ClassConstructorDescriptor) : ClassConstructorDescriptor by original { + override fun isActual(): Boolean = true + override fun isExpect(): Boolean = false + + override fun getSource(): SourceElement = SourceElement.NO_SOURCE + override fun getOriginal(): ClassConstructorDescriptor = this +} + +private class FakeActualFunctionDescriptor(original: FunctionDescriptor) : FunctionDescriptor by original { + override fun isActual(): Boolean = true + override fun isExpect(): Boolean = false + + // `actual` functions are stubbed without providing a body. Hence, they may not be inlined, even if the `expect` function is marked as + // `inline`. Given that inlining requires meaningful bodies (assuming the generated bytecode is of interest), it does not suffice to + // just supply an empty body stub. + override fun isInline(): Boolean = false + + override fun getSource(): SourceElement = SourceElement.NO_SOURCE + override fun getOriginal(): FunctionDescriptor = this +}