From 9b9f0412cedf113c763717f92111351d1ce54b49 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Thu, 24 May 2018 00:09:36 +0300 Subject: [PATCH] Rework Objective-C header generation (#1618) Provide simple tree representation for generated header to be used in IDE for cross-resolve. Also do some cleanup. --- .../konan/objcexport/CustomTypeMapper.kt | 62 ++ .../backend/konan/objcexport/MethodBridge.kt | 102 ++ .../backend/konan/objcexport/ObjCExport.kt | 4 +- .../objcexport/ObjCExportHeaderGenerator.kt | 901 ++++++++---------- .../ObjCExportHeaderGeneratorImpl.kt | 22 + .../konan/objcexport/ObjCExportMapper.kt | 129 +-- .../konan/objcexport/ObjCExportNamer.kt | 21 +- .../backend/konan/objcexport/StubBuilder.kt | 20 + .../backend/konan/objcexport/StubRenderer.kt | 179 ++++ .../backend/konan/objcexport/objcTypes.kt | 129 +++ .../kotlin/backend/konan/objcexport/stubs.kt | 53 ++ 11 files changed, 969 insertions(+), 653 deletions(-) create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/CustomTypeMapper.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGeneratorImpl.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubBuilder.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt create mode 100644 backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/CustomTypeMapper.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/CustomTypeMapper.kt new file mode 100644 index 00000000000..df1748029a4 --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/CustomTypeMapper.kt @@ -0,0 +1,62 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.builtins.getReceiverTypeFromFunctionType +import org.jetbrains.kotlin.builtins.getReturnTypeFromFunctionType +import org.jetbrains.kotlin.builtins.getValueParameterTypesFromFunctionType +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeUtils + +internal interface CustomTypeMapper { + val mappedClassDescriptor: ClassDescriptor + fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType + + class Simple( + override val mappedClassDescriptor: ClassDescriptor, + private val objCClassName: String + ) : CustomTypeMapper { + + override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType = + ObjCClassType(objCClassName) + } + + class Collection( + private val generator: ObjCExportHeaderGenerator, + override val mappedClassDescriptor: ClassDescriptor, + private val objCClassName: String + ) : CustomTypeMapper { + override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType { + val typeArguments = mappedSuperType.arguments.map { + val argument = it.type + if (TypeUtils.isNullableType(argument)) { + // Kotlin `null` keys and values are represented as `NSNull` singleton. + ObjCIdType + } else { + generator.mapReferenceTypeIgnoringNullability(argument) + } + } + + return ObjCClassType(objCClassName, typeArguments) + } + } + + class Function( + private val generator: ObjCExportHeaderGenerator, + parameterCount: Int + ) : CustomTypeMapper { + override val mappedClassDescriptor = generator.builtIns.getFunction(parameterCount) + + override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType { + val functionType = mappedSuperType + + val returnType = functionType.getReturnTypeFromFunctionType() + val parameterTypes = listOfNotNull(functionType.getReceiverTypeFromFunctionType()) + + functionType.getValueParameterTypesFromFunctionType().map { it.type } + + return ObjCBlockPointerType( + generator.mapReferenceType(returnType), + parameterTypes.map { generator.mapReferenceType(it) } + ) + } + } +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt new file mode 100644 index 00000000000..eb5a3c237a6 --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/MethodBridge.kt @@ -0,0 +1,102 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.backend.common.descriptors.allParameters +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.ParameterDescriptor + +internal sealed class TypeBridge +internal object ReferenceBridge : TypeBridge() +internal data class ValueTypeBridge(val objCValueType: ObjCValueType) : TypeBridge() + +internal sealed class MethodBridgeParameter + +internal sealed class MethodBridgeReceiver : MethodBridgeParameter() { + object Static : MethodBridgeReceiver() + object Factory : MethodBridgeReceiver() + object Instance : MethodBridgeReceiver() +} + +internal object MethodBridgeSelector : MethodBridgeParameter() + +internal sealed class MethodBridgeValueParameter : MethodBridgeParameter() { + data class Mapped(val bridge: TypeBridge) : MethodBridgeValueParameter() + object ErrorOutParameter : MethodBridgeValueParameter() + data class KotlinResultOutParameter(val bridge: TypeBridge) : MethodBridgeValueParameter() +} + +internal data class MethodBridge( + val returnBridge: ReturnValue, + val receiver: MethodBridgeReceiver, + val valueParameters: List +) { + + sealed class ReturnValue { + object Void : ReturnValue() + object HashCode : ReturnValue() + data class Mapped(val bridge: TypeBridge) : ReturnValue() + sealed class Instance : ReturnValue() { + object InitResult : Instance() + object FactoryResult : Instance() + } + + sealed class WithError : ReturnValue() { + object Success : WithError() + data class RefOrNull(val successBridge: ReturnValue) : WithError() + } + } + + val paramBridges: List = + listOf(receiver) + MethodBridgeSelector + valueParameters + + // TODO: it is not exactly true in potential future cases. + val isInstance: Boolean get() = when (receiver) { + MethodBridgeReceiver.Static, + MethodBridgeReceiver.Factory -> false + + MethodBridgeReceiver.Instance -> true + } +} + +internal fun MethodBridge.valueParametersAssociated( + descriptor: FunctionDescriptor +): List> { + val kotlinParameters = descriptor.allParameters.iterator() + val skipFirstKotlinParameter = when (this.receiver) { + MethodBridgeReceiver.Static -> false + MethodBridgeReceiver.Factory, MethodBridgeReceiver.Instance -> true + } + if (skipFirstKotlinParameter) { + kotlinParameters.next() + } + + return this.valueParameters.map { + when (it) { + is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next() + + is MethodBridgeValueParameter.ErrorOutParameter, + is MethodBridgeValueParameter.KotlinResultOutParameter -> it to null + } + }.also { assert(!kotlinParameters.hasNext()) } +} + +internal fun MethodBridge.parametersAssociated( + descriptor: FunctionDescriptor +): List> { + val kotlinParameters = descriptor.allParameters.iterator() + + return this.paramBridges.map { + when (it) { + is MethodBridgeValueParameter.Mapped, MethodBridgeReceiver.Instance -> + it to kotlinParameters.next() + + MethodBridgeReceiver.Static, MethodBridgeSelector, MethodBridgeValueParameter.ErrorOutParameter, + is MethodBridgeValueParameter.KotlinResultOutParameter -> + it to null + + MethodBridgeReceiver.Factory -> { + kotlinParameters.next() + it to null + } + } + }.also { assert(!kotlinParameters.hasNext()) } +} diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt index 201add23f73..7365f428b6d 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExport.kt @@ -46,7 +46,7 @@ internal class ObjCExport(val codegen: CodeGenerator) { val topLevelDeclarations: Map> if (context.config.produce == CompilerOutputKind.FRAMEWORK) { - val headerGenerator = ObjCExportHeaderGenerator(context) + val headerGenerator = ObjCExportHeaderGeneratorImpl(context) produceFrameworkSpecific(headerGenerator) generatedClasses = headerGenerator.generatedClasses @@ -63,7 +63,7 @@ internal class ObjCExport(val codegen: CodeGenerator) { } - val namer = ObjCExportNamer(context, mapper) + val namer = ObjCExportNamer(context.moduleDescriptor, context.builtIns, mapper) objCCodeGenerator = ObjCExportCodeGenerator(codegen, namer, mapper) generatedClasses = emptySet() diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt index 09545c9a653..1accf9fe23d 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGenerator.kt @@ -18,12 +18,9 @@ package org.jetbrains.kotlin.backend.konan.objcexport import org.jetbrains.kotlin.backend.konan.* import org.jetbrains.kotlin.backend.konan.descriptors.* -import org.jetbrains.kotlin.builtins.getReceiverTypeFromFunctionType -import org.jetbrains.kotlin.builtins.getReturnTypeFromFunctionType -import org.jetbrains.kotlin.builtins.getValueParameterTypesFromFunctionType +import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.incremental.components.NoLookupLocation -import org.jetbrains.kotlin.ir.util.report import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.constants.ArrayValue @@ -35,24 +32,28 @@ import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.utils.addIfNotNull -internal class ObjCExportHeaderGenerator(val context: Context) { - val mapper: ObjCExportMapper = object : ObjCExportMapper() { +abstract class ObjCExportHeaderGenerator( + val moduleDescriptor: ModuleDescriptor, + val builtIns: KotlinBuiltIns, + topLevelNamePrefix: String = moduleDescriptor.namePrefix +) { + internal val mapper: ObjCExportMapper = object : ObjCExportMapper() { override fun getCategoryMembersFor(descriptor: ClassDescriptor) = extensions[descriptor].orEmpty() override fun isSpecialMapped(descriptor: ClassDescriptor): Boolean { // TODO: this method duplicates some of the [mapReferenceType] logic. - return descriptor == context.builtIns.any || - descriptor.getAllSuperClassifiers().any { it in customTypeMappers } + return descriptor == builtIns.any || + descriptor.getAllSuperClassifiers().any { it in customTypeMappers } } } - val namer = ObjCExportNamer(context, mapper) + internal val namer = ObjCExportNamer(moduleDescriptor, builtIns, mapper, topLevelNamePrefix) - val generatedClasses = mutableSetOf() - val topLevel = mutableMapOf>() + internal val generatedClasses = mutableSetOf() + internal val topLevel = mutableMapOf>() - val customTypeMappers: Map = with (context.builtIns) { + private val customTypeMappers: Map = with(builtIns) { val result = mutableListOf() val generator = this@ObjCExportHeaderGenerator @@ -73,41 +74,77 @@ internal class ObjCExportHeaderGenerator(val context: Context) { result += CustomTypeMapper.Simple(string, "NSString") - (0 .. mapper.maxFunctionTypeParameterCount).forEach { + (0..mapper.maxFunctionTypeParameterCount).forEach { result += CustomTypeMapper.Function(generator, it) } result.associateBy { it.mappedClassDescriptor } } - val hiddenTypes: Set = run { + private val hiddenTypes: Set = run { val customMappedTypes = customTypeMappers.keys customMappedTypes - .flatMap { it.getAllSuperClassifiers().toList() } + .asSequence() + .flatMap { it.getAllSuperClassifiers().asSequence() } .map { it as ClassDescriptor } - .toSet() - customMappedTypes + .filter { !customMappedTypes.contains(it) } + .toSet() } private val kotlinAnyName = namer.kotlinAnyName - private val stubs = mutableListOf() + private val stubs = mutableListOf>() private val classOrInterfaceToName = mutableMapOf() - internal val classForwardDeclarations = mutableSetOf() - internal val protocolForwardDeclarations = mutableSetOf() + private val classForwardDeclarations = mutableSetOf() + private val protocolForwardDeclarations = mutableSetOf() private val extensions = mutableMapOf>() - val extraClassesToTranslate = mutableSetOf() + private val extraClassesToTranslate = mutableSetOf() - fun translateModule() { + fun translateModule(): List> { // TODO: make the translation order stable // to stabilize name mangling. - val packageFragments = context.moduleDescriptor.getPackageFragments() + stubs.add(ObjCInterface(kotlinAnyName, superClass = "NSObject", members = buildMembers { + +ObjCMethod(null, true, ObjCInstanceType, listOf("init"), emptyList(), listOf("unavailable")) + +ObjCMethod(null, false, ObjCInstanceType, listOf("new"), emptyList(), listOf("unavailable")) + +ObjCMethod(null, false, ObjCVoidType, listOf("initialize"), emptyList(), listOf("objc_requires_super")) + })) + + // TODO: add comment to the header. + stubs.add(ObjCInterface( + kotlinAnyName, + superProtocols = listOf("NSCopying"), + categoryName = "${kotlinAnyName}Copying" + )) + + // TODO: only if appears + stubs.add(ObjCInterface( + namer.mutableSetName, + generics = listOf("ObjectType"), + superClass = "NSMutableSet", + attributes = listOf("objc_runtime_name(\"KotlinMutableSet\")") + )) + + // TODO: only if appears + stubs.add(ObjCInterface( + namer.mutableMapName, + generics = listOf("KeyType", "ObjectType"), + superClass = "NSMutableDictionary", + attributes = listOf("objc_runtime_name(\"KotlinMutableDictionary\")") + )) + + stubs.add(ObjCInterface("NSError", categoryName = "NSErrorKotlinException", members = buildMembers { + +ObjCProperty("kotlinException", null, ObjCNullableReferenceType(ObjCIdType), listOf("readonly")) + })) + + val packageFragments = moduleDescriptor.getPackageFragments() packageFragments.forEach { packageFragment -> packageFragment.getMemberScope().getContributedDescriptors() + .asSequence() .filterIsInstance() .filter { mapper.shouldBeExposed(it) } .forEach { @@ -121,18 +158,21 @@ internal class ObjCExportHeaderGenerator(val context: Context) { } - fun MemberScope.translateClasses(): Unit = this.getContributedDescriptors() - .filterIsInstance() - .filter { mapper.shouldBeExposed(it) } - .forEach { - if (it.isInterface) { - translateInterface(it) - } else { - translateClass(it) - } + fun MemberScope.translateClasses() { + getContributedDescriptors() + .asSequence() + .filterIsInstance() + .filter { mapper.shouldBeExposed(it) } + .forEach { + if (it.isInterface) { + translateInterface(it) + } else { + translateClass(it) + } - it.unsubstitutedMemberScope.translateClasses() - } + it.unsubstitutedMemberScope.translateClasses() + } + } packageFragments.forEach { packageFragment -> packageFragment.getMemberScope().translateClasses() @@ -155,9 +195,11 @@ internal class ObjCExportHeaderGenerator(val context: Context) { translateClass(descriptor) } } + + return stubs } - fun translateClassName(descriptor: ClassDescriptor): String = classOrInterfaceToName.getOrPut(descriptor) { + private fun translateClassName(descriptor: ClassDescriptor): String = classOrInterfaceToName.getOrPut(descriptor) { assert(mapper.shouldBeExposed(descriptor)) val forwardDeclarations = if (descriptor.isInterface) protocolForwardDeclarations else classForwardDeclarations @@ -167,54 +209,49 @@ internal class ObjCExportHeaderGenerator(val context: Context) { private fun translateInterface(descriptor: ClassDescriptor) { if (!generatedClasses.add(descriptor)) return - val name = translateClassName(descriptor) + val name: String = translateClassName(descriptor) + val members: List> = buildMembers { translateClassOrInterfaceMembers(descriptor) } + val superProtocols: List = descriptor.superProtocols - stubs.addBuiltBy { - +"@protocol $name${descriptor.superProtocolsClause}" - +"@required" + val protocolStub = ObjCProtocol(name, descriptor, superProtocols, members) - translateClassOrInterfaceMembers(descriptor) - - +"@end;" - } + stubs.add(protocolStub) } - private val ClassDescriptor.superProtocolsClause: String get() { - val interfaces = this.getSuperInterfaces().filter { mapper.shouldBeExposed(it) } - return if (interfaces.isEmpty()) { - "" - } else buildString { - append(" <") - interfaces.joinTo(this) { - translateInterface(it) - translateClassName(it) - } - append(">") - } - } + private val ClassDescriptor.superProtocols: List + get() = + getSuperInterfaces() + .asSequence() + .filter { mapper.shouldBeExposed(it) } + .map { + translateInterface(it) + translateClassName(it) + } + .toList() private fun translateExtensions(classDescriptor: ClassDescriptor, declarations: List) { translateClass(classDescriptor) - stubs.addBuiltBy { - +"@interface ${translateClassName(classDescriptor)} (Extensions)" - + val name = translateClassName(classDescriptor) + val members = buildMembers { translateMembers(declarations) - - +"@end;" } + stubs.add(ObjCInterface(name, categoryName = "Extensions", members = members)) } private fun translateTopLevel(packageFqName: FqName, declarations: List) { val name = namer.getPackageName(packageFqName) - stubs.addBuiltBy { - +"__attribute__((objc_subclassing_restricted))" - +"@interface $name : ${namer.kotlinAnyName}" // TODO: stop inheriting KotlinBase. + // TODO: stop inheriting KotlinBase. + val members = buildMembers { translateMembers(declarations) - - +"@end;" } + stubs.add(ObjCInterface( + name, + superClass = namer.kotlinAnyName, + members = members, + attributes = listOf("objc_subclassing_restricted") + )) } private fun translateClass(descriptor: ClassDescriptor) { @@ -223,53 +260,54 @@ internal class ObjCExportHeaderGenerator(val context: Context) { val name = translateClassName(descriptor) val superClass = descriptor.getSuperClassNotAny() - val superName = if (superClass == null) { + val superName: String = if (superClass == null) { kotlinAnyName } else { translateClass(superClass) translateClassName(superClass) } - stubs.addBuiltBy { - if (descriptor.isFinalOrEnum) { - +"__attribute__((objc_subclassing_restricted))" - } - - +"@interface $name : $superName${descriptor.superProtocolsClause}" - + val superProtocols: List = descriptor.superProtocols + val members: List> = buildMembers { val presentConstructors = mutableSetOf() - descriptor.constructors.filter { mapper.shouldBeExposed(it) }.forEach { - val selector = getSelector(it) - if (!descriptor.isArray) presentConstructors += selector + descriptor.constructors + .asSequence() + .filter { mapper.shouldBeExposed(it) } + .forEach { + val selector = getSelector(it) + if (!descriptor.isArray) presentConstructors += selector - +"${getSignature(it, it)};" - if (selector == "init") { - +"+ (instancetype)new OBJC_SWIFT_UNAVAILABLE(\"use object initializers instead\");" - } - +"" - } + +buildMethod(it, it) + if (selector == "init") { + +ObjCMethod(it, false, ObjCInstanceType, listOf("new"), emptyList(), + listOf("availability(swift, unavailable, message=\"use object initializers instead\")")) + } + } if (descriptor.isArray || descriptor.kind == ClassKind.OBJECT || descriptor.kind == ClassKind.ENUM_CLASS) { - +"+(instancetype)alloc __attribute__((unavailable));" - +"+(instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));" - +"" + +ObjCMethod(null, false, ObjCInstanceType, listOf("alloc"), emptyList(), listOf("unavailable")) + + val parameter = ObjCParameter("zone", null, ObjCRawType("struct _NSZone *")) + +ObjCMethod(descriptor, false, ObjCInstanceType, listOf("allocWithZone:"), listOf(parameter), listOf("unavailable")) } // TODO: consider adding exception-throwing impls for these. when (descriptor.kind) { ClassKind.OBJECT -> { - +"+(instancetype)${namer.getObjectInstanceSelector(descriptor)} NS_SWIFT_NAME(init());" - +"" + +ObjCMethod( + null, false, ObjCInstanceType, + listOf(namer.getObjectInstanceSelector(descriptor)), emptyList(), + listOf(swiftNameAttribute("init()")) + ) } ClassKind.ENUM_CLASS -> { val type = mapType(descriptor.defaultType, ReferenceBridge) descriptor.enumEntries.forEach { val entryName = namer.getEnumEntrySelector(it) - +"@property (class, readonly) ${type.render(entryName)};" + +ObjCProperty(entryName, null, type, listOf("class", "readonly")) } - +"" } else -> { // Nothing special. @@ -277,33 +315,68 @@ internal class ObjCExportHeaderGenerator(val context: Context) { } // Hide "unimplemented" super constructors: - superClass?.constructors?.filter { mapper.shouldBeExposed(it) }?.forEach { - val selector = getSelector(it) - if (selector !in presentConstructors) { - +"${getSignature(it, it)} __attribute__((unavailable));" - if (selector == "init") { - +"+(instancetype) new __attribute__((unavailable));" + superClass?.constructors + ?.asSequence() + ?.filter { mapper.shouldBeExposed(it) } + ?.forEach { + val selector = getSelector(it) + if (selector !in presentConstructors) { + val c = buildMethod(it, it) + +ObjCMethod(c.descriptor, c.isInstanceMethod, c.returnType, c.selectors, c.parameters, c.attributes + "unavailable") + + if (selector == "init") { + +ObjCMethod(null, false, ObjCInstanceType, listOf("new"), emptyList(), listOf("unavailable")) + } + + // TODO: consider adding exception-throwing impls for these. + } } - +"" - // TODO: consider adding exception-throwing impls for these. - } - } - translateClassOrInterfaceMembers(descriptor) - - +"@end;" } + + val attributes = if (descriptor.isFinalOrEnum) listOf("objc_subclassing_restricted") else emptyList() + + val interfaceStub = ObjCInterface( + name, + descriptor = descriptor, + superClass = superName, + superProtocols = superProtocols, + members = members, + attributes = attributes + ) + stubs.add(interfaceStub) } private fun StubBuilder.translateClassOrInterfaceMembers(descriptor: ClassDescriptor) { val members = descriptor.unsubstitutedMemberScope.getContributedDescriptors() + .asSequence() .filterIsInstance() .filter { mapper.shouldBeExposed(it) } + .toList() translateMembers(members) } + private class RenderedStub>(val stub: T) { + private val presentation: String by lazy(LazyThreadSafetyMode.NONE) { + val listOfLines = StubRenderer.render(stub) + assert(listOfLines.size == 1) + listOfLines[0] + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return other is RenderedStub<*> && presentation == other.presentation + } + + override fun hashCode(): Int { + return presentation.hashCode() + } + } + private fun StubBuilder.translateMembers(members: List) { // TODO: add some marks about modality. @@ -323,52 +396,56 @@ internal class ObjCExportHeaderGenerator(val context: Context) { } } - methods.forEach { method -> - val superSignatures = method.overriddenDescriptors + collectMethodsOrProperties(methods) { it -> buildMethods(it.original) } + collectMethodsOrProperties(properties) { it -> buildProperties(it.original) } + } + + private fun > StubBuilder.collectMethodsOrProperties( + members: List, + converter: (D) -> Set>) { + members.forEach { member -> + val superMembers: Set> = (member.overriddenDescriptors as Collection) + .asSequence() .filter { mapper.shouldBeExposed(it) } - .flatMap { getSignatures(it.original) } + .flatMap { converter(it).asSequence() } .toSet() - (getSignatures(method) - superSignatures).forEach { - +"$it;" - } - } - - properties.forEach { property -> - val superSignatures = property.overriddenDescriptors - .filter { mapper.shouldBeExposed(it) } - .flatMap { getSignatures(it.original) } - .toSet() - - getSignatures(property).filter { it !in superSignatures }.forEach { - +"$it;" - } + this += converter(member) + .asSequence() + .filterNot { superMembers.contains(it) } + .map { rendered -> rendered.stub } + .toList() } } - private val methodToSignatures = mutableMapOf>() + private val methodToSignatures = mutableMapOf>>() + private val propertyToSignatures = mutableMapOf>>() - private fun getSignatures(method: FunctionDescriptor) = methodToSignatures.getOrPut(method) { - mapper.getBaseMethods(method).distinctBy { namer.getSelector(it) }.map { base -> - getSignature(method, base) - }.toSet() + private fun buildMethods(method: FunctionDescriptor): Set> = methodToSignatures.getOrPut(method) { + mapper.getBaseMethods(method) + .asSequence() + .distinctBy { namer.getSelector(it) } + .map { base -> buildMethod(method, base) } + .map { method -> RenderedStub(method) } + .toSet() } - private val propertyToSignatures = mutableMapOf>() - - private fun getSignatures(property: PropertyDescriptor) = propertyToSignatures.getOrPut(property) { - mapper.getBaseProperties(property).distinctBy { namer.getName(it) }.map { base -> - getSignature(property, base) - }.toSet() + private fun buildProperties(property: PropertyDescriptor): Set> = propertyToSignatures.getOrPut(property) { + mapper.getBaseProperties(property) + .asSequence() + .distinctBy { namer.getName(it) } + .map { base -> buildProperty(property, base) } + .map { property -> RenderedStub(property) } + .toSet() } // TODO: consider checking that signatures for bases with same selector/name are equal. - fun getSelector(method: FunctionDescriptor): String { + private fun getSelector(method: FunctionDescriptor): String { return namer.getSelector(method) } - private fun getSignature(property: PropertyDescriptor, baseProperty: PropertyDescriptor) = buildString { + private fun buildProperty(property: PropertyDescriptor, baseProperty: PropertyDescriptor): ObjCProperty { assert(mapper.isBaseProperty(baseProperty)) assert(mapper.isObjCProperty(baseProperty)) @@ -376,117 +453,111 @@ internal class ObjCExportHeaderGenerator(val context: Context) { val type = mapReturnType(getterBridge.returnBridge, property.getter!!) val name = namer.getName(baseProperty) - append("@property ") - val attributes = mutableListOf() if (!getterBridge.isInstance) { attributes += "class" } - val getterSelector = getSelector(baseProperty.getter!!) - if (getterSelector != name) { - attributes += "getter=$getterSelector" - } - + val setterName: String? val propertySetter = property.setter if (propertySetter != null && mapper.shouldBeExposed(propertySetter)) { val setterSelector = mapper.getBaseMethods(propertySetter).map { namer.getSelector(it) }.distinct().single() - if (setterSelector != "set" + name.capitalize() + ":") { - attributes += "setter=$setterSelector" - } + setterName = if (setterSelector != "set" + name.capitalize() + ":") setterSelector else null } else { attributes += "readonly" + setterName = null } - if (attributes.isNotEmpty()) { - attributes.joinTo(this, prefix = "(", postfix = ") ") - } + val getterSelector = getSelector(baseProperty.getter!!) + val getterName: String? = if (getterSelector != name) getterSelector else null - append(type.render(name)) + return ObjCProperty(name, property, type, attributes, setterName, getterName) } - private fun getSignature(method: FunctionDescriptor, baseMethod: FunctionDescriptor) = buildString { + private fun buildMethod(method: FunctionDescriptor, baseMethod: FunctionDescriptor): ObjCMethod { + fun collectParameters(baseMethodBridge: MethodBridge, method: FunctionDescriptor): List { + fun unifyName(initialName: String, usedNames: Set): String { + var unique = initialName + while (unique in usedNames) { + unique += "_" + } + return unique + } + + val valueParametersAssociated = baseMethodBridge.valueParametersAssociated(method) + + val parameters = mutableListOf() + + val usedNames = mutableSetOf() + valueParametersAssociated.forEach { (bridge: MethodBridgeValueParameter, p: ParameterDescriptor?) -> + val candidateName: String = when (bridge) { + is MethodBridgeValueParameter.Mapped -> { + p!! + when { + p is ReceiverParameterDescriptor -> "receiver" + method is PropertySetterDescriptor -> "value" + else -> p.name.asString() + } + } + MethodBridgeValueParameter.ErrorOutParameter -> "error" + is MethodBridgeValueParameter.KotlinResultOutParameter -> "result" + } + + val uniqueName = unifyName(candidateName, usedNames) + usedNames += uniqueName + + val type = when (bridge) { + is MethodBridgeValueParameter.Mapped -> mapType(p!!.type, bridge.bridge) + MethodBridgeValueParameter.ErrorOutParameter -> + ObjCPointerType(ObjCNullableReferenceType(ObjCClassType("NSError")), nullable = true) + + is MethodBridgeValueParameter.KotlinResultOutParameter -> + ObjCPointerType(mapType(method.returnType!!, bridge.bridge), nullable = true) + } + + parameters += ObjCParameter(uniqueName, p, type) + } + return parameters + } + assert(mapper.isBaseMethod(baseMethod)) - val methodBridge = mapper.bridgeMethod(baseMethod) + + val baseMethodBridge = mapper.bridgeMethod(baseMethod) exportThrownFromThisAndOverridden(method) - val selectorParts = getSelector(baseMethod).split(':') - - if (methodBridge.isInstance) { - append("-") - } else { - append("+") - } - - append("(") - append(mapReturnType(methodBridge.returnBridge, method).render()) - append(")") - - val valueParametersAssociated = methodBridge.valueParametersAssociated(method) - - val valueParameterNames = mutableListOf() - - valueParametersAssociated.forEach { (bridge, p) -> - var candidate = when (bridge) { - is MethodBridgeValueParameter.Mapped -> { - p!! - when { - p is ReceiverParameterDescriptor -> "receiver" - method is PropertySetterDescriptor -> "value" - else -> p.name.asString() - } - } - MethodBridgeValueParameter.ErrorOutParameter -> "error" - is MethodBridgeValueParameter.KotlinResultOutParameter -> "result" - } - while (candidate in valueParameterNames) { - candidate += "_" - } - valueParameterNames += candidate - } - - append(selectorParts[0]) - - valueParametersAssociated.forEachIndexed { index, (bridge, p) -> - val name = valueParameterNames[index] - val type = when (bridge) { - is MethodBridgeValueParameter.Mapped -> mapType(p!!.type, bridge.bridge) - MethodBridgeValueParameter.ErrorOutParameter -> - ObjCPointerType(ObjCNullableReferenceType(ObjCClassType("NSError")), nullable = true) - - is MethodBridgeValueParameter.KotlinResultOutParameter -> - ObjCPointerType(mapType(method.returnType!!, bridge.bridge), nullable = true) - } - - if (index != 0) { - append(' ') - append(selectorParts[index]) - } - - append(":") - append("(") - append(type.render()) - append(")") - append(name) - } - + val isInstanceMethod: Boolean = baseMethodBridge.isInstance + val returnType: ObjCType = mapReturnType(baseMethodBridge.returnBridge, method) + val parameters = collectParameters(baseMethodBridge, method) + val selector = getSelector(baseMethod) + val selectorParts: List = splitSelector(selector) val swiftName = namer.getSwiftName(baseMethod) + val attributes = mutableListOf() - append(" NS_SWIFT_NAME($swiftName)") + attributes += swiftNameAttribute(swiftName) if (method is ConstructorDescriptor && !method.constructedClass.isArray) { // TODO: check methodBridge instead. - append(" NS_DESIGNATED_INITIALIZER") + attributes += "objc_designated_initializer" } - // TODO: consider adding swift_error attribute. + return ObjCMethod(method, isInstanceMethod, returnType, selectorParts, parameters, attributes) } + private fun splitSelector(selector: String): List { + return if (!selector.endsWith(":")) { + listOf(selector) + } else { + selector.trimEnd(':').split(':').map { "$it:" } + } + } + + private fun swiftNameAttribute(swiftName: String) = "swift_name(\"$swiftName\")" + private val methodsWithThrowAnnotationConsidered = mutableSetOf() private val uncheckedExceptionClasses = listOf("Error", "RuntimeException").map { - context.builtIns.builtInsPackageScope + builtIns.builtInsPackageScope .getContributedClassifier(Name.identifier(it), NoLookupLocation.FROM_BACKEND) as ClassDescriptor } @@ -495,11 +566,7 @@ internal class ObjCExportHeaderGenerator(val context: Context) { val throwsAnnotation = method.annotations.findAnnotation(KonanBuiltIns.FqNames.throws) ?: return if (!mapper.doesThrow(method)) { - context.report( - context.ir.get(method), - "@${KonanBuiltIns.FqNames.throws.shortName()} annotation should also be added to a base method", - isError = false - ) + reportWarning(method, "@${KonanBuiltIns.FqNames.throws.shortName()} annotation should also be added to a base method") } if (method in methodsWithThrowAnnotationConsidered) return @@ -510,13 +577,10 @@ internal class ObjCExportHeaderGenerator(val context: Context) { val classDescriptor = TypeUtils.getClassDescriptor((argument as KClassValue).value) ?: continue uncheckedExceptionClasses.firstOrNull { classDescriptor.isSubclassOf(it) }?.let { - context.report( - context.ir.get(method), + reportWarning(method, "Method is declared to throw ${classDescriptor.fqNameSafe}, " + "but instances of ${it.fqNameSafe} and its subclasses aren't propagated " + - "from Kotlin to Objective-C/Swift", - isError = false - ) + "from Kotlin to Objective-C/Swift") } scheduleClassToBeGenerated(classDescriptor) @@ -527,19 +591,14 @@ internal class ObjCExportHeaderGenerator(val context: Context) { method.allOverriddenDescriptors.forEach { exportThrown(it) } } - private fun mapReturnType( - returnBridge: MethodBridge.ReturnValue, - method: FunctionDescriptor - ): ObjCType = when (returnBridge) { + private fun mapReturnType(returnBridge: MethodBridge.ReturnValue, method: FunctionDescriptor): ObjCType = when (returnBridge) { MethodBridge.ReturnValue.Void -> ObjCVoidType MethodBridge.ReturnValue.HashCode -> ObjCPrimitiveType("NSUInteger") is MethodBridge.ReturnValue.Mapped -> mapType(method.returnType!!, returnBridge.bridge) MethodBridge.ReturnValue.WithError.Success -> ObjCPrimitiveType("BOOL") is MethodBridge.ReturnValue.WithError.RefOrNull -> { - val successReturnType = mapReturnType(returnBridge.successBridge, method) - if (successReturnType !is ObjCNonNullReferenceType) { - error("Function is expected to have non-null return type: $method") - } + val successReturnType = mapReturnType(returnBridge.successBridge, method) as? ObjCNonNullReferenceType + ?: error("Function is expected to have non-null return type: $method") ObjCNullableReferenceType(successReturnType) } @@ -565,315 +624,127 @@ internal class ObjCExportHeaderGenerator(val context: Context) { add("NS_ASSUME_NONNULL_BEGIN") add("") - add("@interface $kotlinAnyName : NSObject") - add("-(instancetype) init __attribute__((unavailable));") - add("+(instancetype) new __attribute__((unavailable));") - add("+(void)initialize __attribute__((objc_requires_super));") - add("@end;") - add("") - - // TODO: add comment to the header. - add("@interface $kotlinAnyName (${kotlinAnyName}Copying) ") - add("@end;") - add("") - - add("__attribute__((objc_runtime_name(\"KotlinMutableSet\")))") - add("@interface ${namer.mutableSetName} : NSMutableSet") // TODO: only if appears - add("@end;") - add("") - - add("__attribute__((objc_runtime_name(\"KotlinMutableDictionary\")))") - add("@interface ${namer.mutableMapName} : NSMutableDictionary") // TODO: only if appears - add("@end;") - add("") - - add("@interface NSError (NSErrorKotlinException)") - add("@property (readonly) id _Nullable kotlinException;") - add("@end;") - add("") - stubs.forEach { - addAll(it.lines) + addAll(StubRenderer.render(it)) add("") } add("NS_ASSUME_NONNULL_END") } -} -internal sealed class ObjCType { - final override fun toString(): String = this.render() + protected abstract fun reportWarning(text: String) - abstract fun render(attrsAndName: String): String + protected abstract fun reportWarning(method: FunctionDescriptor, text: String) - fun render() = render("") - - protected fun String.withAttrsAndName(attrsAndName: String) = - if (attrsAndName.isEmpty()) this else "$this ${attrsAndName.trimStart()}" -} - -internal sealed class ObjCReferenceType : ObjCType() - -internal sealed class ObjCNonNullReferenceType : ObjCReferenceType() - -internal data class ObjCNullableReferenceType(val nonNullType: ObjCNonNullReferenceType) : ObjCReferenceType() { - override fun render(attrsAndName: String) = nonNullType.render(" _Nullable".withAttrsAndName(attrsAndName)) -} - -private class ObjCClassType( - val className: String, - val typeArguments: List = emptyList() -) : ObjCNonNullReferenceType() { - - override fun render(attrsAndName: String) = buildString { - append(className) - if (typeArguments.isNotEmpty()) { - append("<") - typeArguments.joinTo(this) { it.render() } - append(">") - } - append(" *") - append(attrsAndName) - } -} - -private class ObjCProtocolType(val protocolName: String) : ObjCNonNullReferenceType() { - - override fun render(attrsAndName: String) = "id<$protocolName>".withAttrsAndName(attrsAndName) -} - -private object ObjCIdType : ObjCNonNullReferenceType() { - override fun render(attrsAndName: String) = "id".withAttrsAndName(attrsAndName) -} - -private object ObjCInstanceType : ObjCNonNullReferenceType() { - override fun render(attrsAndName: String): String = "instancetype".withAttrsAndName(attrsAndName) -} - -private class ObjCBlockPointerType( - val returnType: ObjCReferenceType, val parameterTypes: List -) : ObjCNonNullReferenceType() { - - override fun render(attrsAndName: String) = returnType.render(buildString { - append("(^") - append(attrsAndName) - append(")(") - if (parameterTypes.isEmpty()) append("void") - parameterTypes.joinTo(this) { it.render() } - append(')') - }) -} - -private class ObjCPrimitiveType(val cName: String) : ObjCType() { - override fun render(attrsAndName: String) = cName.withAttrsAndName(attrsAndName) -} - -private class ObjCPointerType(val pointee: ObjCType, val nullable: Boolean = false) : ObjCType() { - override fun render(attrsAndName: String) = - pointee.render("*${if (nullable) { - " _Nullable".withAttrsAndName(attrsAndName) - } else { - attrsAndName - }}") -} - -private object ObjCVoidType : ObjCType() { - override fun render(attrsAndName: String) = "void".withAttrsAndName(attrsAndName) -} - -internal interface CustomTypeMapper { - val mappedClassDescriptor: ClassDescriptor - fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType - - class Simple( - override val mappedClassDescriptor: ClassDescriptor, - private val objCClassName: String - ) : CustomTypeMapper { - - override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType = - ObjCClassType(objCClassName) - } - - class Collection( - private val generator: ObjCExportHeaderGenerator, - override val mappedClassDescriptor: ClassDescriptor, - private val objCClassName: String - ) : CustomTypeMapper { - override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType { - val typeArguments = mappedSuperType.arguments.map { - val argument = it.type - if (TypeUtils.isNullableType(argument)) { - // Kotlin `null` keys and values are represented as `NSNull` singleton. - ObjCIdType + internal fun mapReferenceType(kotlinType: KotlinType): ObjCReferenceType = + mapReferenceTypeIgnoringNullability(kotlinType).let { + if (TypeUtils.isNullableType(kotlinType)) { + ObjCNullableReferenceType(it) } else { - generator.mapReferenceTypeIgnoringNullability(argument) + it } } - return ObjCClassType(objCClassName, typeArguments) - } - } - - class Function( - private val generator: ObjCExportHeaderGenerator, - parameterCount: Int - ) : CustomTypeMapper { - override val mappedClassDescriptor = generator.context.builtIns.getFunction(parameterCount) - - override fun mapType(mappedSuperType: KotlinType): ObjCNonNullReferenceType { - val functionType = mappedSuperType - - val returnType = functionType.getReturnTypeFromFunctionType() - val parameterTypes = listOfNotNull(functionType.getReceiverTypeFromFunctionType()) + - functionType.getValueParameterTypesFromFunctionType().map { it.type } - - return ObjCBlockPointerType( - generator.mapReferenceType(returnType), - parameterTypes.map { generator.mapReferenceType(it) } - ) - } - } -} - -private fun ObjCExportHeaderGenerator.mapReferenceType(kotlinType: KotlinType): ObjCReferenceType = - mapReferenceTypeIgnoringNullability(kotlinType).let { - if (TypeUtils.isNullableType(kotlinType)) { - ObjCNullableReferenceType(it) + internal fun mapReferenceTypeIgnoringNullability(kotlinType: KotlinType): ObjCNonNullReferenceType { + val typeToMapper = (listOf(kotlinType) + kotlinType.supertypes()).mapNotNull { type -> + val mapper = customTypeMappers[type.constructor.declarationDescriptor] + if (mapper != null) { + type to mapper } else { - it + null + } + }.toMap() + + val mostSpecificTypeToMapper = typeToMapper.filter { (_, mapper) -> + typeToMapper.values.all { + it.mappedClassDescriptor == mapper.mappedClassDescriptor || + !it.mappedClassDescriptor.isSubclassOf(mapper.mappedClassDescriptor) + } + + // E.g. if both List and MutableList are present, then retain only MutableList. + } + + if (mostSpecificTypeToMapper.size > 1) { + val types = mostSpecificTypeToMapper.keys.toList() + val firstType = types[0] + val secondType = types[1] + + reportWarning("Exposed type '$kotlinType' is '$firstType' and '$secondType' at the same time. " + + "This most likely wouldn't work as expected.") + + // TODO: the same warning for such classes. + } + + mostSpecificTypeToMapper.entries.firstOrNull()?.let { (type, mapper) -> + return mapper.mapType(type) + } + + val classDescriptor = kotlinType.getErasedTypeClass() + + // TODO: translate `where T : BaseClass, T : SomeInterface` to `BaseClass* ` + + if (classDescriptor == builtIns.any || classDescriptor in hiddenTypes) { + return ObjCIdType + } + + if (classDescriptor.defaultType.isObjCObjectType()) { + return mapObjCObjectReferenceTypeIgnoringNullability(classDescriptor) + } + + scheduleClassToBeGenerated(classDescriptor) + + return if (classDescriptor.isInterface) { + ObjCProtocolType(translateClassName(classDescriptor)) + } else { + ObjCClassType(translateClassName(classDescriptor)) + } + } + + private tailrec fun mapObjCObjectReferenceTypeIgnoringNullability(descriptor: ClassDescriptor): ObjCNonNullReferenceType { + // TODO: more precise types can be used. + + if (descriptor.isObjCMetaClass()) return ObjCIdType + + if (descriptor.isExternalObjCClass()) { + return if (descriptor.isInterface) { + val name = descriptor.name.asString().removeSuffix("Protocol") + protocolForwardDeclarations += name + ObjCProtocolType(name) + } else { + val name = descriptor.name.asString() + classForwardDeclarations += name + ObjCClassType(name) } } -private fun ObjCExportHeaderGenerator.mapReferenceTypeIgnoringNullability( - kotlinType: KotlinType -): ObjCNonNullReferenceType { - val typeToMapper = (listOf(kotlinType) + kotlinType.supertypes()).mapNotNull { type -> - val mapper = customTypeMappers[type.constructor.declarationDescriptor] - if (mapper != null) { - type to mapper - } else { - null + if (descriptor.isKotlinObjCClass()) { + return mapObjCObjectReferenceTypeIgnoringNullability(descriptor.getSuperClassOrAny()) } - }.toMap() - val mostSpecificTypeToMapper = typeToMapper.filter { (_, mapper) -> - typeToMapper.values.all { it.mappedClassDescriptor == mapper.mappedClassDescriptor || - !it.mappedClassDescriptor.isSubclassOf(mapper.mappedClassDescriptor) } - - // E.g. if both List and MutableList are present, then retain only MutableList. - } - - if (mostSpecificTypeToMapper.size > 1) { - val types = mostSpecificTypeToMapper.keys.toList() - val firstType = types[0] - val secondType = types[1] - - context.reportCompilationWarning( - "Exposed type '$kotlinType' is '$firstType' and '$secondType' at the same time. " + - "This most likely wouldn't work as expected.") - - // TODO: the same warning for such classes. - } - - mostSpecificTypeToMapper.entries.firstOrNull()?.let { (type, mapper) -> - return mapper.mapType(type) - } - - val classDescriptor = kotlinType.getErasedTypeClass() - - // TODO: translate `where T : BaseClass, T : SomeInterface` to `BaseClass* ` - - if (classDescriptor == context.builtIns.any || classDescriptor in hiddenTypes) { return ObjCIdType } - if (classDescriptor.defaultType.isObjCObjectType()) { - return mapObjCObjectReferenceTypeIgnoringNullability(classDescriptor) - } - - scheduleClassToBeGenerated(classDescriptor) - - return if (classDescriptor.isInterface) { - ObjCProtocolType(translateClassName(classDescriptor)) - } else { - ObjCClassType(translateClassName(classDescriptor)) - } -} - -private tailrec fun ObjCExportHeaderGenerator.mapObjCObjectReferenceTypeIgnoringNullability( - descriptor: ClassDescriptor -): ObjCNonNullReferenceType { - // TODO: more precise types can be used. - - if (descriptor.isObjCMetaClass()) return ObjCIdType - - if (descriptor.isExternalObjCClass()) { - return if (descriptor.isInterface) { - val name = descriptor.name.asString().removeSuffix("Protocol") - protocolForwardDeclarations += name - ObjCProtocolType(name) - } else { - val name = descriptor.name.asString() - classForwardDeclarations += name - ObjCClassType(name) + private fun scheduleClassToBeGenerated(classDescriptor: ClassDescriptor) { + if (classDescriptor !in generatedClasses) { + extraClassesToTranslate += classDescriptor } } - if (descriptor.isKotlinObjCClass()) { - return mapObjCObjectReferenceTypeIgnoringNullability(descriptor.getSuperClassOrAny()) - } - - return ObjCIdType -} - -private fun ObjCExportHeaderGenerator.scheduleClassToBeGenerated(classDescriptor: ClassDescriptor) { - if (classDescriptor !in generatedClasses) { - extraClassesToTranslate += classDescriptor - } -} - -private fun ObjCExportHeaderGenerator.mapType( - kotlinType: KotlinType, - typeBridge: TypeBridge -): ObjCType = when (typeBridge) { - ReferenceBridge -> mapReferenceType(kotlinType) - is ValueTypeBridge -> { - val cName = when (typeBridge.objCValueType) { - ObjCValueType.BOOL -> "BOOL" - ObjCValueType.CHAR -> "int8_t" - ObjCValueType.UNSIGNED_SHORT -> "unichar" - ObjCValueType.SHORT -> "int16_t" - ObjCValueType.INT -> "int32_t" - ObjCValueType.LONG_LONG -> "int64_t" - ObjCValueType.FLOAT -> "float" - ObjCValueType.DOUBLE -> "double" + private fun mapType(kotlinType: KotlinType, typeBridge: TypeBridge): ObjCType = when (typeBridge) { + ReferenceBridge -> mapReferenceType(kotlinType) + is ValueTypeBridge -> { + val cName = when (typeBridge.objCValueType) { + ObjCValueType.BOOL -> "BOOL" + ObjCValueType.CHAR -> "int8_t" + ObjCValueType.UNSIGNED_SHORT -> "unichar" + ObjCValueType.SHORT -> "int16_t" + ObjCValueType.INT -> "int32_t" + ObjCValueType.LONG_LONG -> "int64_t" + ObjCValueType.FLOAT -> "float" + ObjCValueType.DOUBLE -> "double" + } + // TODO: consider other namings. + ObjCPrimitiveType(cName) } - // TODO: consider other namings. - ObjCPrimitiveType(cName) } -} - -private data class Stub(val lines: List) - -private class StubBuilder { - private val lines = mutableListOf() - - operator fun String.unaryPlus() { - lines.add(this) - } - - operator fun Stub.unaryPlus() { - this@StubBuilder.lines.addAll(this.lines) - } - - fun build() = Stub(lines) -} - -private inline fun buildStub(block: StubBuilder.() -> Unit) = StubBuilder().let { - it.block() - it.build() -} - -private inline fun MutableCollection.addBuiltBy(block: StubBuilder.() -> Unit) { - this.add(buildStub(block)) -} +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGeneratorImpl.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGeneratorImpl.kt new file mode 100644 index 00000000000..2a1994d0f54 --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportHeaderGeneratorImpl.kt @@ -0,0 +1,22 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.backend.konan.Context +import org.jetbrains.kotlin.backend.konan.reportCompilationWarning +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.ir.util.report + +internal class ObjCExportHeaderGeneratorImpl(val context: Context) + : ObjCExportHeaderGenerator(context.moduleDescriptor, context.builtIns) { + + override fun reportWarning(text: String) { + context.reportCompilationWarning(text) + } + + override fun reportWarning(method: FunctionDescriptor, text: String) { + context.report( + context.ir.get(method), + text, + isError = false + ) + } +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt index 41561dc9799..b348bab7acd 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportMapper.kt @@ -36,7 +36,7 @@ internal abstract class ObjCExportMapper { private val methodBridgeCache = mutableMapOf() - fun bridgeMethod(descriptor: FunctionDescriptor) = methodBridgeCache.getOrPut(descriptor) { + fun bridgeMethod(descriptor: FunctionDescriptor): MethodBridge = methodBridgeCache.getOrPut(descriptor) { bridgeMethodImpl(descriptor) } } @@ -110,105 +110,6 @@ internal fun ObjCExportMapper.doesThrow(method: FunctionDescriptor): Boolean = m it.overriddenDescriptors.isEmpty() && it.annotations.hasAnnotation(KonanBuiltIns.FqNames.throws) } -// TODO: generalize type bridges to support such things as selectors, ignored class method receivers etc. - -internal sealed class TypeBridge -internal object ReferenceBridge : TypeBridge() -internal data class ValueTypeBridge(val objCValueType: ObjCValueType) : TypeBridge() - -internal sealed class MethodBridgeParameter - -internal sealed class MethodBridgeReceiver : MethodBridgeParameter() { - object Static : MethodBridgeReceiver() - object Factory : MethodBridgeReceiver() - object Instance : MethodBridgeReceiver() -} - -internal object MethodBridgeSelector : MethodBridgeParameter() - -internal sealed class MethodBridgeValueParameter : MethodBridgeParameter() { - data class Mapped(val bridge: TypeBridge) : MethodBridgeValueParameter() - object ErrorOutParameter : MethodBridgeValueParameter() - data class KotlinResultOutParameter(val bridge: TypeBridge) : MethodBridgeValueParameter() -} - -internal data class MethodBridge( - val returnBridge: ReturnValue, - val receiver: MethodBridgeReceiver, - val valueParameters: List -) { - - sealed class ReturnValue { - object Void : ReturnValue() - object HashCode : ReturnValue() - data class Mapped(val bridge: TypeBridge) : ReturnValue() - sealed class Instance : ReturnValue() { - object InitResult : Instance() - object FactoryResult : Instance() - } - - sealed class WithError : ReturnValue() { - object Success : WithError() - data class RefOrNull(val successBridge: ReturnValue) : WithError() - } - } - - val paramBridges: List = - listOf(receiver) + MethodBridgeSelector + valueParameters - - // TODO: it is not exactly true in potential future cases. - val isInstance: Boolean get() = when (receiver) { - MethodBridgeReceiver.Static, - MethodBridgeReceiver.Factory -> false - - MethodBridgeReceiver.Instance -> true - } -} - -internal fun MethodBridge.valueParametersAssociated( - descriptor: FunctionDescriptor -): List> { - val kotlinParameters = descriptor.allParameters.iterator() - val skipFirstKotlinParameter = when (this.receiver) { - MethodBridgeReceiver.Static -> false - MethodBridgeReceiver.Factory, MethodBridgeReceiver.Instance -> true - } - if (skipFirstKotlinParameter) { - kotlinParameters.next() - } - - return this.valueParameters.map { - when (it) { - is MethodBridgeValueParameter.Mapped -> it to kotlinParameters.next() - - is MethodBridgeValueParameter.ErrorOutParameter, - is MethodBridgeValueParameter.KotlinResultOutParameter -> it to null - } - }.also { assert(!kotlinParameters.hasNext()) } -} - -internal fun MethodBridge.parametersAssociated( - descriptor: FunctionDescriptor -): List> { - val kotlinParameters = descriptor.allParameters.iterator() - - return this.paramBridges.map { - when (it) { - is MethodBridgeValueParameter.Mapped, MethodBridgeReceiver.Instance -> - it to kotlinParameters.next() - - MethodBridgeReceiver.Static, MethodBridgeSelector, MethodBridgeValueParameter.ErrorOutParameter, - is MethodBridgeValueParameter.KotlinResultOutParameter -> - it to null - - MethodBridgeReceiver.Factory -> { - kotlinParameters.next() - it to null - } - } - }.also { assert(!kotlinParameters.hasNext()) } -} - private fun ObjCExportMapper.bridgeType(kotlinType: KotlinType): TypeBridge { val valueType = kotlinType.correspondingValueType ?: return ReferenceBridge @@ -305,30 +206,4 @@ internal fun ObjCExportMapper.bridgePropertyType(descriptor: PropertyDescriptor) assert(isBaseProperty(descriptor)) return bridgeType(descriptor.type) -} - -internal enum class ObjCValueType( - val kotlinValueType: ValueType, // It is here for simplicity. - val encoding: String -) { - - BOOL(ValueType.BOOLEAN, "c"), - CHAR(ValueType.BYTE, "c"), - UNSIGNED_SHORT(ValueType.CHAR, "S"), - SHORT(ValueType.SHORT, "s"), - INT(ValueType.INT, "i"), - LONG_LONG(ValueType.LONG, "q"), - FLOAT(ValueType.FLOAT, "f"), - DOUBLE(ValueType.DOUBLE, "d") - - ; - - // UNSIGNED_SHORT -> unsignedShort - val nsNumberName = this.name.split('_').mapIndexed { index, s -> - val lower = s.toLowerCase() - if (index > 0) lower.capitalize() else lower - }.joinToString("") - - val nsNumberValueSelector get() = "${nsNumberName}Value" - val nsNumberFactorySelector get() = "numberWith${nsNumberName.capitalize()}:" -} +} \ No newline at end of file diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt index fa298a30cad..dec84addeb3 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/ObjCExportNamer.kt @@ -16,9 +16,9 @@ package org.jetbrains.kotlin.backend.konan.objcexport -import org.jetbrains.kotlin.backend.konan.Context import org.jetbrains.kotlin.backend.konan.descriptors.isArray import org.jetbrains.kotlin.backend.konan.descriptors.isInterface +import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.name.FqName @@ -28,11 +28,14 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.isSubclassOf import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf -internal class ObjCExportNamer(val context: Context, val mapper: ObjCExportMapper) { +internal class ObjCExportNamer(val moduleDescriptor: ModuleDescriptor, + val builtIns: KotlinBuiltIns, + val mapper: ObjCExportMapper, + private val topLevelNamePrefix: String = moduleDescriptor.namePrefix + ) { val kotlinAnyName = "KotlinBase" - private val commonPackageSegments = context.moduleDescriptor.guessMainPackage().pathSegments() - private val topLevelNamePrefix = context.moduleDescriptor.namePrefix + private val commonPackageSegments = moduleDescriptor.guessMainPackage().pathSegments() val mutableSetName = "${topLevelNamePrefix}MutableSet" val mutableMapName = "${topLevelNamePrefix}MutableDictionary" @@ -116,7 +119,7 @@ internal class ObjCExportNamer(val context: Context, val mapper: ObjCExportMappe StringBuilder().apply { append(topLevelNamePrefix) - if (descriptor.module != context.moduleDescriptor) { + if (descriptor.module != moduleDescriptor) { append(descriptor.module.namePrefix) } @@ -246,11 +249,11 @@ internal class ObjCExportNamer(val context: Context, val mapper: ObjCExportMappe } init { - val any = context.builtIns.any + val any = builtIns.any classNames.forceAssign(any, kotlinAnyName) - classNames.forceAssign(context.builtIns.mutableSet, mutableSetName) - classNames.forceAssign(context.builtIns.mutableMap, mutableMapName) + classNames.forceAssign(builtIns.mutableSet, mutableSetName) + classNames.forceAssign(builtIns.mutableMap, mutableMapName) fun ClassDescriptor.method(name: String) = this.unsubstitutedMemberScope.getContributedFunctions( @@ -439,7 +442,7 @@ private fun ObjCExportMapper.canHaveSameName(first: PropertyDescriptor, second: return bridgePropertyType(first) == bridgePropertyType(second) } -private val ModuleDescriptor.namePrefix: String get() { +internal val ModuleDescriptor.namePrefix: String get() { // -> FooBar val moduleName = this.name.asString().let { it.substring(1, it.lastIndex) }.capitalize() diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubBuilder.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubBuilder.kt new file mode 100644 index 00000000000..71e25846255 --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubBuilder.kt @@ -0,0 +1,20 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +internal class StubBuilder { + private val children = mutableListOf>() + + operator fun Stub<*>.unaryPlus() { + children.add(this) + } + + operator fun plusAssign(set: Collection>) { + children += set + } + + fun build() = children +} + +internal inline fun buildMembers(block: StubBuilder.() -> Unit): List> = StubBuilder().let { + it.block() + it.build() +} diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt new file mode 100644 index 00000000000..fd76885d2bd --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/StubRenderer.kt @@ -0,0 +1,179 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.descriptors.ClassDescriptor + +object StubRenderer { + fun render(stub: Stub<*>): List = collect { + stub.run { + when (this) { + is ObjCProtocol -> { + +renderProtocolHeader() + +"@required" + renderMembers(this) + +"@end;" + } + is ObjCInterface -> { + attributes.forEach { + +renderAttribute(it) + } + +renderInterfaceHeader() + renderMembers(this) + +"@end;" + } + is ObjCMethod -> { + +renderMethod(this) + } + is ObjCProperty -> { + +renderProperty(this) + } + else -> throw IllegalArgumentException("unsupported stub: " + stub::class) + } + } + } + + private fun renderProperty(property: ObjCProperty): String = buildString { + fun StringBuilder.appendTypeAndName() { + append(' ') + append(property.type.render(property.name)) + } + + fun ObjCProperty.getAllAttributes(): List { + if (getterName == null && setterName == null) return attributes + + val allAttributes = attributes.toMutableList() + getterName?.let { allAttributes += "getter=$it" } + setterName?.let { allAttributes += "setter=$it" } + return allAttributes + } + + fun StringBuilder.appendAttributes() { + val attributes = property.getAllAttributes() + if (attributes.isNotEmpty()) { + append(' ') + attributes.joinTo(this, prefix = "(", postfix = ")") + } + } + + append("@property") + appendAttributes() + appendTypeAndName() + append(';') + } + + private fun renderMethod(method: ObjCMethod): String = buildString { + fun appendStaticness() { + if (method.isInstanceMethod) { + append('-') + } else { + append('+') + } + } + + fun appendReturnType() { + append(" (") + append(method.returnType.render()) + append(')') + } + + fun appendParameters() { + assert(method.selectors.size == method.parameters.size || + method.selectors.size == 1 && method.parameters.size == 0) + + if (method.selectors.size == 1 && method.parameters.size == 0) { + append(method.selectors[0]) + } else { + for (i in 0 until method.selectors.size) { + if (i > 0) append(' ') + + val parameter = method.parameters[i] + val selector = method.selectors[i] + append(selector) + append("(") + append(parameter.type.render()) + append(")") + append(parameter.name) + } + } + } + + fun appendAttributes() { + if (method.attributes.isNotEmpty()) append(' ') + method.attributes.joinTo(this, separator = " ", transform = ::renderAttribute) + } + + appendStaticness() + appendReturnType() + appendParameters() + appendAttributes() + append(';') + } + + private fun ObjCProtocol.renderProtocolHeader() = buildString { + append("@protocol ") + append(name) + appendSuperProtocols(this@renderProtocolHeader) + } + + private fun StringBuilder.appendSuperProtocols(clazz: ObjCClass) { + val protocols = clazz.superProtocols + if (protocols.isNotEmpty()) { + protocols.joinTo(this, separator = ", ", prefix = " <", postfix = ">") + } + } + + private fun ObjCInterface.renderInterfaceHeader() = buildString { + fun appendSuperClass() { + if (superClass != null) append(" : $superClass") + } + + fun appendGenerics() { + val generics = generics + if (generics.isNotEmpty()) { + generics.joinTo(this, separator = ", ", prefix = "<", postfix = ">") + } + } + + fun appendCategoryName() { + if (categoryName != null) { + append(" (") + append(categoryName) + append(')') + } + } + + append("@interface ") + append(name) + appendGenerics() + appendCategoryName() + appendSuperClass() + appendSuperProtocols(this@renderInterfaceHeader) + } + + private fun Collector.renderMembers(clazz: ObjCClass<*>) { + clazz.members.forEach { + +render(it) + } + } + + private fun renderAttribute(attribute: String) = "__attribute__(($attribute))" + + private fun collect(p: Collector.() -> Unit): List { + val collector = Collector() + collector.p() + return collector.build() + } + + private class Collector { + private val collection: MutableList = mutableListOf() + fun build(): List = collection + + operator fun String.unaryPlus() { + collection += this + } + + operator fun List.unaryPlus() { + collection += this + } + } +} + diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt new file mode 100644 index 00000000000..aa117f897fc --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/objcTypes.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.backend.konan.ValueType + +sealed class ObjCType { + final override fun toString(): String = this.render() + + abstract fun render(attrsAndName: String): String + + fun render() = render("") + + protected fun String.withAttrsAndName(attrsAndName: String) = + if (attrsAndName.isEmpty()) this else "$this ${attrsAndName.trimStart()}" +} + +internal class ObjCRawType(private val rawText: String) : ObjCType() { + override fun render(attrsAndName: String): String = rawText.withAttrsAndName(attrsAndName) +} + +internal sealed class ObjCReferenceType : ObjCType() + +internal sealed class ObjCNonNullReferenceType : ObjCReferenceType() + +internal data class ObjCNullableReferenceType(val nonNullType: ObjCNonNullReferenceType) : ObjCReferenceType() { + override fun render(attrsAndName: String) = nonNullType.render(" _Nullable".withAttrsAndName(attrsAndName)) +} + +internal class ObjCClassType( + val className: String, + val typeArguments: List = emptyList() +) : ObjCNonNullReferenceType() { + + override fun render(attrsAndName: String) = buildString { + append(className) + if (typeArguments.isNotEmpty()) { + append("<") + typeArguments.joinTo(this) { it.render() } + append(">") + } + append(" *") + append(attrsAndName) + } +} + +internal class ObjCProtocolType(val protocolName: String) : ObjCNonNullReferenceType() { + + override fun render(attrsAndName: String) = "id<$protocolName>".withAttrsAndName(attrsAndName) +} + +internal object ObjCIdType : ObjCNonNullReferenceType() { + override fun render(attrsAndName: String) = "id".withAttrsAndName(attrsAndName) +} + +internal object ObjCInstanceType : ObjCNonNullReferenceType() { + override fun render(attrsAndName: String): String = "instancetype".withAttrsAndName(attrsAndName) +} + +internal class ObjCBlockPointerType( + val returnType: ObjCReferenceType, val parameterTypes: List +) : ObjCNonNullReferenceType() { + + override fun render(attrsAndName: String) = returnType.render(buildString { + append("(^") + append(attrsAndName) + append(")(") + if (parameterTypes.isEmpty()) append("void") + parameterTypes.joinTo(this) { it.render() } + append(')') + }) +} + +internal class ObjCPrimitiveType(val cName: String) : ObjCType() { + override fun render(attrsAndName: String) = cName.withAttrsAndName(attrsAndName) +} + +internal class ObjCPointerType(val pointee: ObjCType, val nullable: Boolean = false) : ObjCType() { + override fun render(attrsAndName: String) = + pointee.render("*${if (nullable) { + " _Nullable".withAttrsAndName(attrsAndName) + } else { + attrsAndName + }}") +} + +internal object ObjCVoidType : ObjCType() { + override fun render(attrsAndName: String) = "void".withAttrsAndName(attrsAndName) +} + +internal enum class ObjCValueType( + val kotlinValueType: ValueType, // It is here for simplicity. + val encoding: String +) { + + BOOL(ValueType.BOOLEAN, "c"), + CHAR(ValueType.BYTE, "c"), + UNSIGNED_SHORT(ValueType.CHAR, "S"), + SHORT(ValueType.SHORT, "s"), + INT(ValueType.INT, "i"), + LONG_LONG(ValueType.LONG, "q"), + FLOAT(ValueType.FLOAT, "f"), + DOUBLE(ValueType.DOUBLE, "d") + + ; + + // UNSIGNED_SHORT -> unsignedShort + val nsNumberName = this.name.split('_').mapIndexed { index, s -> + val lower = s.toLowerCase() + if (index > 0) lower.capitalize() else lower + }.joinToString("") + + val nsNumberValueSelector get() = "${nsNumberName}Value" + val nsNumberFactorySelector get() = "numberWith${nsNumberName.capitalize()}:" +} diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt new file mode 100644 index 00000000000..a7c19e8b07e --- /dev/null +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/objcexport/stubs.kt @@ -0,0 +1,53 @@ +package org.jetbrains.kotlin.backend.konan.objcexport + +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.ParameterDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor + +open class Stub(val name: String, val descriptor: D?) + +abstract class ObjCClass(name: String, + descriptor: D?, + val superProtocols: List, + val members: List>) : Stub(name, descriptor) + +class ObjCProtocol(name: String, + descriptor: ClassDescriptor, + superProtocols: List, + members: List>) : ObjCClass(name, descriptor, superProtocols, members) + +class ObjCInterface(name: String, + val generics: List = emptyList(), + descriptor: ClassDescriptor? = null, + val superClass: String? = null, + superProtocols: List = emptyList(), + val categoryName: String? = null, + members: List> = emptyList(), + val attributes: List = emptyList()) : ObjCClass(name, descriptor, superProtocols, members) + +class ObjCMethod(descriptor: DeclarationDescriptor?, + val isInstanceMethod: Boolean, + val returnType: ObjCType, + val selectors: List, + val parameters: List, + val attributes: List) : Stub(buildMethodName(selectors, parameters), descriptor) + +class ObjCParameter(name: String, + descriptor: ParameterDescriptor?, + val type: ObjCType) : Stub(name, descriptor) + +class ObjCProperty(name: String, + descriptor: PropertyDescriptor?, + val type: ObjCType, + val attributes: List, + val setterName: String? = null, + val getterName: String? = null) : Stub(name, descriptor) + +private fun buildMethodName(selectors: List, parameters: List): String = + if (selectors.size == 1 && parameters.size == 0) { + selectors[0] + } else { + assert(selectors.size == parameters.size) + selectors.joinToString(separator = "") + }