Skip to content

Commit 555edcf

Browse files
committed
Fix: Bind member functions of klib classes and interfaces (#237)
KlibScanner.memberCandidatesOf builds #215's METHOD entries for the walked klib's handle classes, with (args[0] as Owner).name(...) and the receiver outside the slots. Any's members and enum statics are skipped; members of non-final classes and objects, data-class synthetics and the other #215 cases are declined with reasons. Closes #237
1 parent a0e088a commit 555edcf

1 file changed

Lines changed: 143 additions & 3 deletions

File tree

  • python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact

‎python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/artifact/KlibScanner.kt‎

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import org.jetbrains.kotlin.library.abi.AbiClassifierReference
66
import org.jetbrains.kotlin.library.abi.AbiDeclarationContainer
77
import org.jetbrains.kotlin.library.abi.AbiCompoundName
88
import org.jetbrains.kotlin.library.abi.AbiFunction
9+
import org.jetbrains.kotlin.library.abi.AbiModality
10+
import org.jetbrains.kotlin.library.abi.AbiProperty
911
import org.jetbrains.kotlin.library.abi.AbiQualifiedName
1012
import org.jetbrains.kotlin.library.abi.AbiType
1113
import org.jetbrains.kotlin.library.abi.AbiTypeArgument
@@ -187,6 +189,7 @@ internal object KlibScanner {
187189
return emptyList()
188190
}
189191

192+
val classes = classesOf(abi.topLevelDeclarations)
190193
val candidates = abi.topLevelDeclarations.declarations
191194
.filterIsInstance<AbiFunction>()
192195
.filter { matchesInclude(it.qualifiedName, includePrefixes) }
@@ -196,10 +199,132 @@ internal object KlibScanner {
196199
// receiver handle to be worth anything. Filtered rather than declined for the same reason
197200
// `ArtifactScanner` never builds a model for `<init>`.
198201
.filter { !it.isConstructor }
199-
.mapNotNull { candidateOrNull(it, classesOf(abi.topLevelDeclarations)) }
202+
.mapNotNull { candidateOrNull(it, classes) }
200203

201-
// Same rule, same implementation as the JVM producer -- see this object's KDoc.
202-
return ArtifactScanner.disambiguateOverloads(candidates)
204+
// Issue #237: the member functions of the klib's classes, as #215 binds them from a jar.
205+
val members = classes.values
206+
.filter { matchesInclude(it.qualifiedName, includePrefixes) }
207+
.filter { !ArtifactScanner.isExcludedPackage(it.qualifiedName.packageName.toString(), excludePackages) }
208+
.flatMap { memberCandidatesOf(it, classes) }
209+
210+
// Same rule, same implementation as the JVM producer -- see this object's KDoc. A member's key
211+
// is `Owner.name`, so its overload set is its class's alone.
212+
return ArtifactScanner.disambiguateOverloads(candidates + members)
213+
}
214+
215+
/**
216+
* The member functions of [cls] (issue #237), as `METHOD` entries `Owner.name` -- the shape #215
217+
* gives a jar's members and KSP gives its own: `args[0]` is the receiver, not counted in `arity`,
218+
* and the generated call is `(args[0] as Owner).name(...)`. The binding layer serves them as
219+
* methods of the receiver's proxy, and never as a module attribute.
220+
*
221+
* The class must be able to cross as a handle ([handleClassOrNull]); a generic one's members are
222+
* declined, as on the jar side. A member is declined, with its reason, when it is `suspend`,
223+
* `@Deprecated`, `@PublishedApi`, `@Composable`, a member extension or one with context receivers,
224+
* named like a property of its class (the property's name is the attribute), or typed by something
225+
* without a klib boundary type. Constructors are not read here (stage 3).
226+
*/
227+
private fun memberCandidatesOf(cls: AbiClass, classes: Map<String, AbiClass>): List<Candidate> {
228+
val owner = cls.qualifiedName.dotted()
229+
val ownerType = KotlinTypeModel(owner)
230+
val generic = cls.typeParameters.isNotEmpty()
231+
if (!generic && cls.kind != AbiClassKind.OBJECT && handleClassOrNull(ownerType, classes) == null) return emptyList()
232+
val propertyNames = cls.declarations.filterIsInstance<AbiProperty>()
233+
.mapTo(HashSet()) { it.qualifiedName.relativeName.toString().substringAfterLast('.') }
234+
// The pinned compiler's klib ABI records modality but no visibility, and lists the `protected`
235+
// members of an open, abstract or sealed class beside its public ones; source outside a subclass
236+
// cannot call a `protected` one. An interface has none, and a final class's are effectively
237+
// private, so only those members bind. An `object`'s functions are statics on the jar side (#53),
238+
// called without a receiver, which this `METHOD` shape is not.
239+
val unbindable = when {
240+
cls.kind == AbiClassKind.OBJECT -> "a function of an object: bound as a static on the jar side (#53), not yet from a klib"
241+
cls.kind == AbiClassKind.CLASS && cls.modality != AbiModality.FINAL ->
242+
"a member of a non-final class: this klib ABI does not say whether it is protected"
243+
else -> null
244+
}
245+
val enum = cls.kind == AbiClassKind.ENUM_CLASS
246+
return cls.declarations.filterIsInstance<AbiFunction>()
247+
.filter { !it.isConstructor }
248+
// `Any`'s members are not the class's API, as on the jar side; an enum class's
249+
// `valueOf`/`values`/`entries` are statics the ABI lists among its members, and no
250+
// instance call reaches them.
251+
.filter { function -> !isAnyMember(function) && !(enum && function.leafName() in ENUM_STATICS) }
252+
.mapNotNull { function -> memberCandidateOrNull(function, owner, ownerType, generic, unbindable, propertyNames, classes) }
253+
}
254+
255+
private fun memberCandidateOrNull(
256+
function: AbiFunction,
257+
owner: String,
258+
ownerType: KotlinTypeModel,
259+
generic: Boolean,
260+
unbindable: String?,
261+
propertyNames: Set<String>,
262+
classes: Map<String, AbiClass>,
263+
): Candidate? {
264+
val name = function.leafName()
265+
val model = declarationModelOf(function)?.copy(
266+
simpleName = name,
267+
owner = owner,
268+
ownerIsClass = true,
269+
receiver = ownerType,
270+
receiverBoundaryTag = OBJECT_TAG,
271+
kind = METHOD_KIND,
272+
) ?: return null
273+
val declined = { reason: String -> Candidate(null, model.copy(declineReason = reason)) }
274+
when {
275+
function.isSuspend -> return declined("suspend")
276+
generic -> return declined("a member of a generic class: not bound yet")
277+
unbindable != null -> return declined(unbindable)
278+
function.hasAnnotation(DEPRECATED) ->
279+
return declined("@Deprecated: its level is not in this klib ABI, and a HIDDEN one cannot be called from source")
280+
function.hasAnnotation(PUBLISHED_API) ->
281+
return declined("@PublishedApi internal -- binary-visible for inlining, not part of the module's public Kotlin API")
282+
function.hasAnnotation(COMPOSABLE) -> return declined("a @Composable member function: not bound on a proxy")
283+
function.hasExtensionReceiverParameter -> return declined("a member extension: it needs two receivers")
284+
function.contextReceiverParametersCount > 0 ->
285+
return declined("${function.contextReceiverParametersCount} context receiver(s), whose types this klib ABI does not expose")
286+
name in propertyNames -> return declined("shares its name with a property of the class, which keeps the attribute")
287+
// A data class's `copy`/`componentN` are synthetics the jar side never binds (U-4), and this
288+
// ABI cannot tell them from a hand-written member of the same name.
289+
name == "copy" || DATA_COMPONENT.matches(name) ->
290+
return declined("a data-class synthetic (copy/componentN), or a member this klib ABI cannot tell from one")
291+
}
292+
val returnType = klibBoundaryTypeOf(model.returnType, classes)
293+
?: return declined("no boundary type for return ${model.returnType.render()}")
294+
val boundaryTypes = model.parameters.map { parameter ->
295+
klibBoundaryTypeOf(parameter.type, classes)
296+
?: return declined("no boundary type for parameter ${parameter.type.render()}")
297+
}
298+
if (boundaryTypes.any { it.isReturnOnly }) return declined("a parameter of a return-only boundary type")
299+
300+
val key = "$owner.$name"
301+
val argumentExpressions = boundaryTypes.mapIndexed { index, type -> type.read("args[${index + 1}]") }
302+
val body = returnType.wrapReturn("(args[0] as $owner).$name(${argumentExpressions.joinToString(", ")})")
303+
val callable = ArtifactCallable(
304+
name = key,
305+
arity = boundaryTypes.size,
306+
paramTags = boundaryTypes.map { it.tag },
307+
returnTag = returnType.tag,
308+
// The receiver is read even with no declared parameter, so the body always takes `args`.
309+
lambdaBody = "{ args -> $body }",
310+
receiverTypeName = owner,
311+
paramTypeNames = model.parameters.map { it.type.qualifiedName },
312+
returnTypeName = model.returnType.qualifiedName,
313+
returnSupertypes = if (returnType.tag == OBJECT_TAG) supertypesOf(model.returnType.qualifiedName, classes) else emptyList(),
314+
// Every argument is passed, as for a top-level klib entry (see [candidateOrNull]).
315+
paramHasDefault = List(boundaryTypes.size) { false },
316+
kind = METHOD_KIND,
317+
)
318+
return Candidate(
319+
callable = callable,
320+
declaration = model.copy(
321+
bindingName = key,
322+
returnBoundaryTag = returnType.tag,
323+
parameters = model.parameters.mapIndexed { index, parameter ->
324+
parameter.copy(boundaryTag = boundaryTypes[index].tag, declaresDefault = false)
325+
},
326+
),
327+
)
203328
}
204329

205330
/**
@@ -471,6 +596,21 @@ internal object KlibScanner {
471596

472597
private const val OBJECT_TAG = "OBJECT"
473598

599+
private const val METHOD_KIND = "METHOD"
600+
601+
private val ENUM_STATICS = setOf("valueOf", "values", "entries")
602+
603+
private val DATA_COMPONENT = Regex("component[1-9][0-9]*")
604+
605+
private fun AbiFunction.leafName(): String = qualifiedName.relativeName.toString().substringAfterLast('.')
606+
607+
/** `toString()`, `hashCode()` and `equals(other)`: what every class has from `Any`. */
608+
private fun isAnyMember(function: AbiFunction): Boolean = when (function.leafName()) {
609+
"toString", "hashCode" -> function.valueParameters.isEmpty()
610+
"equals" -> function.valueParameters.size == 1
611+
else -> false
612+
}
613+
474614
/**
475615
* The walked klib's class [type] names, when a handle of it can cross (issue #234): a public
476616
* (the ABI lists nothing else) `class`, `interface`, `enum class` or `object` that is not a value

0 commit comments

Comments
 (0)