From d0e67ff336d5c3af28785653f21cb2227a14a2b5 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Fri, 16 Feb 2024 10:04:39 +0100 Subject: [PATCH] [ObjCExport] Restructure ObjC export symbol queue to replicate order of K1 The previous implementation used a two-stage process: 1) Processing of declared symbols 2) Analysis and processing of referenced dependency symbols However, to replicate the exact same order as in K1 those two steps need to be done together. ^KT-64953 Fixed --- .../getAllClassOrObjectSymbols.kt | 2 +- .../objcexport/translateToObjCExportStub.kt | 15 +- .../objcexport/translateToObjCHeader.kt | 258 +++++++++++------- 3 files changed, 168 insertions(+), 107 deletions(-) diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getAllClassOrObjectSymbols.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getAllClassOrObjectSymbols.kt index be379e6a3be..fe9b4b2b52d 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getAllClassOrObjectSymbols.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/analysisApiUtils/getAllClassOrObjectSymbols.kt @@ -20,7 +20,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol * returns `sequenceOf(A, B, C)` */ context(KtAnalysisSession) -internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List { +internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List { return getFileScope().getClassifierSymbols() .filterIsInstance() .flatMap { classSymbol -> listOf(classSymbol) + classSymbol.getAllClassOrObjectSymbols() } diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt index 9b21c62edc1..ce2e74fd9bc 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCExportStub.kt @@ -1,9 +1,8 @@ package org.jetbrains.kotlin.objcexport import org.jetbrains.kotlin.analysis.api.KtAnalysisSession -import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtPropertySymbol +import org.jetbrains.kotlin.analysis.api.symbols.* +import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClass import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub context(KtAnalysisSession, KtObjCExportSession) @@ -14,3 +13,13 @@ internal fun KtCallableSymbol.translateToObjCExportStub(): ObjCExportStub? { else -> null } } + +context(KtAnalysisSession, KtObjCExportSession) +internal fun KtClassOrObjectSymbol.translateToObjCExportStub(): ObjCClass? = when (classKind) { + KtClassKind.INTERFACE -> translateToObjCProtocol() + KtClassKind.CLASS -> translateToObjCClass() + KtClassKind.OBJECT -> translateToObjCObject() + KtClassKind.ENUM_CLASS -> translateToObjCClass() + KtClassKind.COMPANION_OBJECT -> translateToObjCObject() + else -> null +} \ No newline at end of file diff --git a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt index 5ae79b262b9..1a9ecb0f658 100644 --- a/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt +++ b/native/objcexport-header-generator/impl/analysis-api/src/org/jetbrains/kotlin/objcexport/translateToObjCHeader.kt @@ -6,136 +6,188 @@ package org.jetbrains.kotlin.objcexport import org.jetbrains.kotlin.analysis.api.KtAnalysisSession -import org.jetbrains.kotlin.analysis.api.symbols.KtClassKind import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol -import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol import org.jetbrains.kotlin.backend.konan.objcexport.* import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.objcexport.KtObjCExportHeaderGenerator.QueueElement import org.jetbrains.kotlin.objcexport.analysisApiUtils.* import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.utils.addIfNotNull + context(KtAnalysisSession, KtObjCExportSession) fun translateToObjCHeader(files: List): ObjCHeader { - val stubs = mutableListOf() - val protocolForwardDeclarations = mutableSetOf() - val classForwardDeclarations = mutableSetOf() + val generator = KtObjCExportHeaderGenerator() + generator.translateAll(files.sortedWith(StableFileOrder).map { QueueElement.File(it) }) + return generator.buildObjCHeader() +} - val symbolTranslationQueue = mutableListOf() - val translatedClassifiers = mutableMapOf() +/** + * Encapsulates the 'dynamic' nature of the ObjCExport where only during the export phase decisions about + * 1) Which symbols are to be exported + * 2) In which order symbols are to be exported + * + * can be made. + * + * Functions inside this class will have side effects such as mutating the [symbolDeque] or adding results to the [objCStubs] + */ +private class KtObjCExportHeaderGenerator { + /** + * Represents all elements that still have to be processed and translated. + * So far this only includes references to top level entities (such as files or classes): + * Note: Top level functions and properties will be translated as part of the file. + * See [translateToObjCTopLevelInterfaceFileFacade] + */ + private val symbolDeque = ArrayDeque() - fun process( - symbol: KtSymbol, - forwardProtocolsAndClasses: Boolean = false, - ): List { - val result = mutableListOf() - when (symbol) { - /* In this case: Go and translate all classes/objects/interfaces inside the file as well */ - is KtFileSymbol -> { - val translatedTopLevelFileFacade = symbol.translateToObjCTopLevelInterfaceFileFacade() - result.addIfNotNull(translatedTopLevelFileFacade) + /** + * The mutable aggregate of the already translated elements + */ + private val objCStubs = mutableListOf() - symbolTranslationQueue.addAll( - symbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder) - ) - } + /** + * An index of all already translated classes. All classes here are also present in [objCStubs] + */ + private val objCStubsByClassId = hashMapOf() - /* Translate the Class/Interface, but ensure that all supertypes will be translated also */ - is KtClassOrObjectSymbol -> { - val classId = symbol.classIdIfNonLocal ?: return result + /** + * The mutable aggregate of all entities that shall later be rendered as forward declarations + */ + private val objCForwardDeclarations = mutableSetOf() - /* Add the classId to already processed classIds and do not redo if already processed */ - val stub = translatedClassifiers.getOrPut(classId) { - val translatedObjCClassOrProtocol = when (symbol.classKind) { - KtClassKind.INTERFACE -> symbol.translateToObjCProtocol() - KtClassKind.CLASS -> symbol.translateToObjCClass() - KtClassKind.OBJECT -> symbol.translateToObjCObject() - KtClassKind.ENUM_CLASS -> symbol.translateToObjCClass() - KtClassKind.COMPANION_OBJECT -> symbol.translateToObjCObject() - else -> return result - } ?: return result - - symbol.getDeclaredSuperInterfaceSymbols().forEach { superInterfaceSymbol -> - result.addAll(process(superInterfaceSymbol, true)) - } - - symbol.getSuperClassSymbolNotAny()?.let { superClassSymbol -> - result.addAll(process(superClassSymbol, true)) - } - - result.add(translatedObjCClassOrProtocol) - translatedObjCClassOrProtocol - } - - if (forwardProtocolsAndClasses) { - when (stub) { - is ObjCInterface -> classForwardDeclarations.add(ObjCClassForwardDeclaration(stub.name, stub.generics)) - is ObjCProtocol -> protocolForwardDeclarations.add(stub.name) - } - } - } - } - return result + /** + * See [symbolDeque]: + * All top level 'to do' elements will be represented as [QueueElement] and later handled by the [translateAll] function. + */ + sealed class QueueElement { + class File(val psi: KtFile) : QueueElement() + class Class(val classId: ClassId) : QueueElement() } - fun processDeclaredSymbols(symbols: List): List { - val result = mutableListOf() - symbolTranslationQueue.addAll(symbols) + context(KtAnalysisSession, KtObjCExportSession) + fun translateAll(symbolProviders: List) { + symbolDeque.addAll(symbolProviders) + while (true) { - val next = symbolTranslationQueue.removeFirstOrNull() ?: break - result.addAll(process(next)) + val symbolProvider = symbolDeque.removeFirstOrNull() ?: break + translateElement(symbolProvider) } - return result } - fun processDependencySymbols(stubs: List): List { - val dependencyClassSymbols = stubs.closureSequence() - .mapNotNull { stub -> - when (stub) { - is ObjCMethod -> stub.returnType - is ObjCParameter -> stub.type - is ObjCProperty -> stub.type - is ObjCTopLevel -> null - } - } - .flatMap { type -> - if (type is ObjCClassType) type.typeArguments + type - else listOf(type) - } - .mapNotNull { if (it is ObjCReferenceType) it.classId else null } - .mapNotNull { classId -> getClassOrObjectSymbolByClassId(classId) } - .toList() + context(KtAnalysisSession, KtObjCExportSession) + private fun translateElement(element: QueueElement) = when (element) { + is QueueElement.Class -> translateClassElement(element) + is QueueElement.File -> translateFileElement(element) + } - symbolTranslationQueue.addAll(dependencyClassSymbols) + context(KtAnalysisSession, KtObjCExportSession) + private fun translateClassElement(element: QueueElement.Class) { + val classOrObjectSymbol = getClassOrObjectSymbolByClassId(element.classId) ?: return + translateClassOrObjectSymbol(classOrObjectSymbol) + } - val result = dependencyClassSymbols.flatMap { symbol -> - process(symbol, forwardProtocolsAndClasses = true) + context(KtAnalysisSession, KtObjCExportSession) + private fun translateFileElement(element: QueueElement.File) { + val fileSymbol = element.psi.getFileSymbol() + translateFileSymbol(fileSymbol) + fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol -> + translateClassOrObjectSymbol(classOrObjectSymbol) + } + } + + context(KtAnalysisSession, KtObjCExportSession) + private fun translateFileSymbol(symbol: KtFileSymbol) { + val objCInterface = symbol.translateToObjCTopLevelInterfaceFileFacade() ?: return + objCStubs += objCInterface + enqueueDependencyClasses(objCInterface) + } + + context(KtAnalysisSession, KtObjCExportSession) + private fun translateClassOrObjectSymbol(symbol: KtClassOrObjectSymbol) { + /* No classId, no stubs ¯\_(ツ)_/¯ */ + val classId = symbol.classIdIfNonLocal ?: return + + /* Already processed this class, therefore nothing to do! */ + if (classId in objCStubsByClassId) return + + /** + * Translate: Note: Even if the result was 'null', the classId will still be marked as 'handled' by adding it + * to the [objCStubsByClassId] index. + */ + val objCClass = symbol.translateToObjCExportStub() + objCStubsByClassId[classId] = objCClass + objCClass ?: return + + /* + To replicate the translation (and result stub order) of the K1 implementation: + 1) Super interface / superclass symbols have to be translated right away + 2) Super interface / superclass symbol export stubs (result of translation) have to be present in the stubs list before the + original stub + */ + val superInterfaceOrClassSymbols = buildList { + addAll(symbol.getDeclaredSuperInterfaceSymbols()) + addIfNotNull(symbol.getSuperClassSymbolNotAny()) } - return if (result.isNotEmpty()) result + processDependencySymbols(result) - else result + superInterfaceOrClassSymbols.forEach { superInterfaceOrClassSymbol -> + translateClassOrObjectSymbol(superInterfaceOrClassSymbol) + } + + /* Note: It is important to add *this* stub to the result list only after translating/processing the superclass symbols */ + objCStubs += objCClass + objCForwardDeclarations += superInterfaceOrClassSymbols.mapNotNull { it.classIdIfNonLocal } + enqueueDependencyClasses(objCClass) } - val fileSymbols = files.sortedWith(StableFileOrder).map { it.getFileSymbol() } - val declaredStubs = processDeclaredSymbols(fileSymbols) - val dependencyStubs = processDependencySymbols(declaredStubs) - - stubs.addAll(declaredStubs + dependencyStubs) - - if (stubs.hasErrorTypes()) { - stubs.add(errorInterface) - classForwardDeclarations.add(errorForwardClass) + /** + * Will introspect the given [stub] to collect all used 'dependency' types/classes. + * Example: Usage of Kotlin Stdlib Type (Array): + * + * ``` + * class Foo { + * fun createArray(): Array = error("stub") + * } + * ``` + * + * The given symbol "Foo" will reference `Array`. Therefore, the `Array` class has to be translated as well (later) + * and `Array` has to be registered as forward declaration. + */ + private fun enqueueDependencyClasses(stub: ObjCExportStub) { + symbolDeque += stub.closureSequence().mapNotNull { child -> + when (child) { + is ObjCMethod -> child.returnType + is ObjCParameter -> child.type + is ObjCProperty -> child.type + is ObjCTopLevel -> null + } + }.flatMap { type -> + if (type is ObjCClassType) type.typeArguments + type + else listOf(type) + }.mapNotNull { if (it is ObjCReferenceType) it.classId else null }.onEach { objCForwardDeclarations += it } + .map { QueueElement.Class(it) }.toList() } - if (configuration.generateBaseDeclarationStubs) { - stubs.addAll(0, objCBaseDeclarations()) - } + context(KtAnalysisSession, KtObjCExportSession) + fun buildObjCHeader(): ObjCHeader { + val hasErrorTypes = objCStubs.hasErrorTypes() - return ObjCHeader( - stubs = stubs, - classForwardDeclarations = classForwardDeclarations, - protocolForwardDeclarations = protocolForwardDeclarations, - additionalImports = emptyList() - ) -} \ No newline at end of file + val resolvedObjCForwardDeclarations = objCForwardDeclarations.mapNotNull { classId -> objCStubsByClassId[classId] }.asSequence() + + val protocolForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance().map { it.name }.toSet() + + val classForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance() + .map { stub -> ObjCClassForwardDeclaration(stub.name, stub.generics) } + .plus(listOfNotNull(errorForwardClass.takeIf { hasErrorTypes })).toSet() + + val stubs = (if (configuration.generateBaseDeclarationStubs) objCBaseDeclarations() else emptyList()).plus(objCStubs) + .plus(listOfNotNull(errorInterface.takeIf { hasErrorTypes })) + + return ObjCHeader( + stubs = stubs, + classForwardDeclarations = classForwardDeclarations, + protocolForwardDeclarations = protocolForwardDeclarations, + additionalImports = emptyList() + ) + } +}