[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)`
*/
context(KtAnalysisSession)
internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List<KtClassifierSymbol> {
internal fun KtFileSymbol.getAllClassOrObjectSymbols(): List<KtClassOrObjectSymbol> {
return getFileScope().getClassifierSymbols()
.filterIsInstance<KtClassOrObjectSymbol>()
.flatMap { classSymbol -> listOf(classSymbol) + classSymbol.getAllClassOrObjectSymbols() }
@@ -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
}
@@ -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<KtFile>): ObjCHeader {
val stubs = mutableListOf<ObjCExportStub>()
val protocolForwardDeclarations = mutableSetOf<String>()
val classForwardDeclarations = mutableSetOf<ObjCClassForwardDeclaration>()
val generator = KtObjCExportHeaderGenerator()
generator.translateAll(files.sortedWith(StableFileOrder).map { QueueElement.File(it) })
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,
forwardProtocolsAndClasses: Boolean = false,
): List<ObjCExportStub> {
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)
/**
* The mutable aggregate of the already translated elements
*/
private val objCStubs = mutableListOf<ObjCTopLevel>()
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 -> {
val classId = symbol.classIdIfNonLocal ?: return result
/**
* The mutable aggregate of all entities that shall later be rendered as forward declarations
*/
private val objCForwardDeclarations = mutableSetOf<ClassId>()
/* 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<KtSymbol>): List<ObjCExportStub> {
val result = mutableListOf<ObjCExportStub>()
symbolTranslationQueue.addAll(symbols)
context(KtAnalysisSession, KtObjCExportSession)
fun translateAll(symbolProviders: List<QueueElement>) {
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<ObjCExportStub>): List<ObjCExportStub> {
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<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()
}
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()
)
}
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()
)
}
}