[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
This commit is contained in:
Sebastian Sellmair
2024-02-16 10:04:39 +01:00
committed by Space Team
parent a2d76d739c
commit d0e67ff336
3 changed files with 168 additions and 107 deletions
@@ -20,7 +20,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol
* returns `sequenceOf(A, B, C)` * returns `sequenceOf(A, B, C)`
*/ */
context(KtAnalysisSession) context(KtAnalysisSession)
internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List<KtClassifierSymbol> { internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List<KtClassOrObjectSymbol> {
return getFileScope().getClassifierSymbols() return getFileScope().getClassifierSymbols()
.filterIsInstance<KtClassOrObjectSymbol>() .filterIsInstance<KtClassOrObjectSymbol>()
.flatMap { classSymbol -> listOf(classSymbol) + classSymbol.getAllClassOrObjectSymbols() } .flatMap { classSymbol -> listOf(classSymbol) + classSymbol.getAllClassOrObjectSymbols() }
@@ -1,9 +1,8 @@
package org.jetbrains.kotlin.objcexport package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol import org.jetbrains.kotlin.analysis.api.symbols.*
import org.jetbrains.kotlin.analysis.api.symbols.KtFunctionSymbol import org.jetbrains.kotlin.backend.konan.objcexport.ObjCClass
import org.jetbrains.kotlin.analysis.api.symbols.KtPropertySymbol
import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub import org.jetbrains.kotlin.backend.konan.objcexport.ObjCExportStub
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
@@ -14,3 +13,13 @@ internal fun KtCallableSymbol.translateToObjCExportStub(): ObjCExportStub? {
else -> null 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
}
@@ -6,136 +6,188 @@
package org.jetbrains.kotlin.objcexport package org.jetbrains.kotlin.objcexport
import org.jetbrains.kotlin.analysis.api.KtAnalysisSession 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.KtClassOrObjectSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtFileSymbol 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.backend.konan.objcexport.*
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.objcexport.KtObjCExportHeaderGenerator.QueueElement
import org.jetbrains.kotlin.objcexport.analysisApiUtils.* import org.jetbrains.kotlin.objcexport.analysisApiUtils.*
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addIfNotNull
context(KtAnalysisSession, KtObjCExportSession) context(KtAnalysisSession, KtObjCExportSession)
fun translateToObjCHeader(files: List<KtFile>): ObjCHeader { fun translateToObjCHeader(files: List<KtFile>): ObjCHeader {
val stubs = mutableListOf<ObjCExportStub>() val generator = KtObjCExportHeaderGenerator()
val protocolForwardDeclarations = mutableSetOf<String>() generator.translateAll(files.sortedWith(StableFileOrder).map { QueueElement.File(it) })
val classForwardDeclarations = mutableSetOf<ObjCClassForwardDeclaration>() return generator.buildObjCHeader()
}
val symbolTranslationQueue = mutableListOf<KtSymbol>()
val translatedClassifiers = mutableMapOf<ClassId, ObjCClass>() /**
* Encapsulates the 'dynamic' nature of the ObjCExport where only during the export phase decisions about
fun process( * 1) Which symbols are to be exported
symbol: KtSymbol, * 2) In which order symbols are to be exported
forwardProtocolsAndClasses: Boolean = false, *
): List<ObjCExportStub> { * can be made.
val result = mutableListOf<ObjCExportStub>() *
when (symbol) { * Functions inside this class will have side effects such as mutating the [symbolDeque] or adding results to the [objCStubs]
/* In this case: Go and translate all classes/objects/interfaces inside the file as well */ */
is KtFileSymbol -> { private class KtObjCExportHeaderGenerator {
val translatedTopLevelFileFacade = symbol.translateToObjCTopLevelInterfaceFileFacade() /**
result.addIfNotNull(translatedTopLevelFileFacade) * 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):
symbolTranslationQueue.addAll( * Note: Top level functions and properties will be translated as part of the file.
symbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder) * See [translateToObjCTopLevelInterfaceFileFacade]
) */
} private val symbolDeque = ArrayDeque<QueueElement>()
/* Translate the Class/Interface, but ensure that all supertypes will be translated also */ /**
is KtClassOrObjectSymbol -> { * The mutable aggregate of the already translated elements
val classId = symbol.classIdIfNonLocal ?: return result */
private val objCStubs = mutableListOf<ObjCTopLevel>()
/* Add the classId to already processed classIds and do not redo if already processed */
val stub = translatedClassifiers.getOrPut(classId) { /**
val translatedObjCClassOrProtocol = when (symbol.classKind) { * An index of all already translated classes. All classes here are also present in [objCStubs]
KtClassKind.INTERFACE -> symbol.translateToObjCProtocol() */
KtClassKind.CLASS -> symbol.translateToObjCClass() private val objCStubsByClassId = hashMapOf<ClassId, ObjCClass?>()
KtClassKind.OBJECT -> symbol.translateToObjCObject()
KtClassKind.ENUM_CLASS -> symbol.translateToObjCClass() /**
KtClassKind.COMPANION_OBJECT -> symbol.translateToObjCObject() * The mutable aggregate of all entities that shall later be rendered as forward declarations
else -> return result */
} ?: return result private val objCForwardDeclarations = mutableSetOf<ClassId>()
symbol.getDeclaredSuperInterfaceSymbols().forEach { superInterfaceSymbol -> /**
result.addAll(process(superInterfaceSymbol, true)) * See [symbolDeque]:
} * All top level 'to do' elements will be represented as [QueueElement] and later handled by the [translateAll] function.
*/
symbol.getSuperClassSymbolNotAny()?.let { superClassSymbol -> sealed class QueueElement {
result.addAll(process(superClassSymbol, true)) class File(val psi: KtFile) : QueueElement()
} class Class(val classId: ClassId) : QueueElement()
}
result.add(translatedObjCClassOrProtocol)
translatedObjCClassOrProtocol context(KtAnalysisSession, KtObjCExportSession)
} fun translateAll(symbolProviders: List<QueueElement>) {
symbolDeque.addAll(symbolProviders)
if (forwardProtocolsAndClasses) {
when (stub) { while (true) {
is ObjCInterface -> classForwardDeclarations.add(ObjCClassForwardDeclaration(stub.name, stub.generics)) val symbolProvider = symbolDeque.removeFirstOrNull() ?: break
is ObjCProtocol -> protocolForwardDeclarations.add(stub.name) translateElement(symbolProvider)
} }
} }
}
} context(KtAnalysisSession, KtObjCExportSession)
return result private fun translateElement(element: QueueElement) = when (element) {
} is QueueElement.Class -> translateClassElement(element)
is QueueElement.File -> translateFileElement(element)
fun processDeclaredSymbols(symbols: List<KtSymbol>): List<ObjCExportStub> { }
val result = mutableListOf<ObjCExportStub>()
symbolTranslationQueue.addAll(symbols) context(KtAnalysisSession, KtObjCExportSession)
while (true) { private fun translateClassElement(element: QueueElement.Class) {
val next = symbolTranslationQueue.removeFirstOrNull() ?: break val classOrObjectSymbol = getClassOrObjectSymbolByClassId(element.classId) ?: return
result.addAll(process(next)) translateClassOrObjectSymbol(classOrObjectSymbol)
} }
return result
} context(KtAnalysisSession, KtObjCExportSession)
private fun translateFileElement(element: QueueElement.File) {
fun processDependencySymbols(stubs: List<ObjCExportStub>): List<ObjCExportStub> { val fileSymbol = element.psi.getFileSymbol()
val dependencyClassSymbols = stubs.closureSequence() translateFileSymbol(fileSymbol)
.mapNotNull { stub -> fileSymbol.getAllClassOrObjectSymbols().sortedWith(StableClassifierOrder).forEach { classOrObjectSymbol ->
when (stub) { translateClassOrObjectSymbol(classOrObjectSymbol)
is ObjCMethod -> stub.returnType }
is ObjCParameter -> stub.type }
is ObjCProperty -> stub.type
is ObjCTopLevel -> null context(KtAnalysisSession, KtObjCExportSession)
} private fun translateFileSymbol(symbol: KtFileSymbol) {
} val objCInterface = symbol.translateToObjCTopLevelInterfaceFileFacade() ?: return
.flatMap { type -> objCStubs += objCInterface
if (type is ObjCClassType) type.typeArguments + type enqueueDependencyClasses(objCInterface)
else listOf(type) }
}
.mapNotNull { if (it is ObjCReferenceType) it.classId else null } context(KtAnalysisSession, KtObjCExportSession)
.mapNotNull { classId -> getClassOrObjectSymbolByClassId(classId) } private fun translateClassOrObjectSymbol(symbol: KtClassOrObjectSymbol) {
.toList() /* No classId, no stubs ¯\_(ツ)_/¯ */
val classId = symbol.classIdIfNonLocal ?: return
symbolTranslationQueue.addAll(dependencyClassSymbols)
/* Already processed this class, therefore nothing to do! */
val result = dependencyClassSymbols.flatMap { symbol -> if (classId in objCStubsByClassId) return
process(symbol, forwardProtocolsAndClasses = true)
} /**
* Translate: Note: Even if the result was 'null', the classId will still be marked as 'handled' by adding it
return if (result.isNotEmpty()) result + processDependencySymbols(result) * to the [objCStubsByClassId] index.
else result */
} val objCClass = symbol.translateToObjCExportStub()
objCStubsByClassId[classId] = objCClass
val fileSymbols = files.sortedWith(StableFileOrder).map { it.getFileSymbol() } objCClass ?: return
val declaredStubs = processDeclaredSymbols(fileSymbols)
val dependencyStubs = processDependencySymbols(declaredStubs) /*
To replicate the translation (and result stub order) of the K1 implementation:
stubs.addAll(declaredStubs + dependencyStubs) 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
if (stubs.hasErrorTypes()) { original stub
stubs.add(errorInterface) */
classForwardDeclarations.add(errorForwardClass) val superInterfaceOrClassSymbols = buildList {
} addAll(symbol.getDeclaredSuperInterfaceSymbols())
addIfNotNull(symbol.getSuperClassSymbolNotAny())
if (configuration.generateBaseDeclarationStubs) { }
stubs.addAll(0, objCBaseDeclarations())
} superInterfaceOrClassSymbols.forEach { superInterfaceOrClassSymbol ->
translateClassOrObjectSymbol(superInterfaceOrClassSymbol)
return ObjCHeader( }
stubs = stubs,
classForwardDeclarations = classForwardDeclarations, /* Note: It is important to add *this* stub to the result list only after translating/processing the superclass symbols */
protocolForwardDeclarations = protocolForwardDeclarations, objCStubs += objCClass
additionalImports = emptyList() objCForwardDeclarations += superInterfaceOrClassSymbols.mapNotNull { it.classIdIfNonLocal }
) enqueueDependencyClasses(objCClass)
}
/**
* Will introspect the given [stub] to collect all used 'dependency' types/classes.
* Example: Usage of Kotlin Stdlib Type (Array):
*
* ```
* class Foo {
* fun createArray(): Array<String> = 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()
}
context(KtAnalysisSession, KtObjCExportSession)
fun buildObjCHeader(): ObjCHeader {
val hasErrorTypes = objCStubs.hasErrorTypes()
val resolvedObjCForwardDeclarations = objCForwardDeclarations.mapNotNull { classId -> objCStubsByClassId[classId] }.asSequence()
val protocolForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCProtocol>().map { it.name }.toSet()
val classForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCInterface>()
.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()
)
}
} }