[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
* 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<QueueElement>()
fun process( /**
symbol: KtSymbol, * The mutable aggregate of the already translated elements
forwardProtocolsAndClasses: Boolean = false, */
): List<ObjCExportStub> { private val objCStubs = mutableListOf<ObjCTopLevel>()
val result = mutableListOf<ObjCExportStub>()
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)
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<ClassId, ObjCClass?>()
/* Translate the Class/Interface, but ensure that all supertypes will be translated also */ /**
is KtClassOrObjectSymbol -> { * The mutable aggregate of all entities that shall later be rendered as forward declarations
val classId = symbol.classIdIfNonLocal ?: return result */
private val objCForwardDeclarations = mutableSetOf<ClassId>()
/* Add the classId to already processed classIds and do not redo if already processed */ /**
val stub = translatedClassifiers.getOrPut(classId) { * See [symbolDeque]:
val translatedObjCClassOrProtocol = when (symbol.classKind) { * All top level 'to do' elements will be represented as [QueueElement] and later handled by the [translateAll] function.
KtClassKind.INTERFACE -> symbol.translateToObjCProtocol() */
KtClassKind.CLASS -> symbol.translateToObjCClass() sealed class QueueElement {
KtClassKind.OBJECT -> symbol.translateToObjCObject() class File(val psi: KtFile) : QueueElement()
KtClassKind.ENUM_CLASS -> symbol.translateToObjCClass() class Class(val classId: ClassId) : QueueElement()
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
} }
fun processDeclaredSymbols(symbols: List<KtSymbol>): List<ObjCExportStub> { context(KtAnalysisSession, KtObjCExportSession)
val result = mutableListOf<ObjCExportStub>() fun translateAll(symbolProviders: List<QueueElement>) {
symbolTranslationQueue.addAll(symbols) symbolDeque.addAll(symbolProviders)
while (true) { while (true) {
val next = symbolTranslationQueue.removeFirstOrNull() ?: break val symbolProvider = symbolDeque.removeFirstOrNull() ?: break
result.addAll(process(next)) translateElement(symbolProvider)
} }
return result
} }
fun processDependencySymbols(stubs: List<ObjCExportStub>): List<ObjCExportStub> { context(KtAnalysisSession, KtObjCExportSession)
val dependencyClassSymbols = stubs.closureSequence() private fun translateElement(element: QueueElement) = when (element) {
.mapNotNull { stub -> is QueueElement.Class -> translateClassElement(element)
when (stub) { is QueueElement.File -> translateFileElement(element)
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()
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 -> context(KtAnalysisSession, KtObjCExportSession)
process(symbol, forwardProtocolsAndClasses = true) 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) superInterfaceOrClassSymbols.forEach { superInterfaceOrClassSymbol ->
else result 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) * Will introspect the given [stub] to collect all used 'dependency' types/classes.
val dependencyStubs = processDependencySymbols(declaredStubs) * Example: Usage of Kotlin Stdlib Type (Array):
*
stubs.addAll(declaredStubs + dependencyStubs) * ```
* class Foo {
if (stubs.hasErrorTypes()) { * fun createArray(): Array<String> = error("stub")
stubs.add(errorInterface) * }
classForwardDeclarations.add(errorForwardClass) * ```
*
* 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) { context(KtAnalysisSession, KtObjCExportSession)
stubs.addAll(0, objCBaseDeclarations()) fun buildObjCHeader(): ObjCHeader {
} val hasErrorTypes = objCStubs.hasErrorTypes()
return ObjCHeader( val resolvedObjCForwardDeclarations = objCForwardDeclarations.mapNotNull { classId -> objCStubsByClassId[classId] }.asSequence()
stubs = stubs,
classForwardDeclarations = classForwardDeclarations, val protocolForwardDeclarations = resolvedObjCForwardDeclarations.filterIsInstance<ObjCProtocol>().map { it.name }.toSet()
protocolForwardDeclarations = protocolForwardDeclarations,
additionalImports = emptyList() 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()
)
}
}