@@ -6,6 +6,7 @@ import kotlin.metadata.KmType
66import kotlin.metadata.KmVariance
77import kotlin.metadata.MemberKind
88import kotlin.metadata.Visibility
9+ import kotlin.metadata.isFunInterface
910import kotlin.metadata.isNullable
1011import kotlin.metadata.isSuspend
1112import kotlin.metadata.isValue
@@ -587,7 +588,9 @@ internal object ArtifactScanner {
587588 // A zero-parameter member of a group -- there can be at most one, so this only ever has to
588589 // be distinct from the others, not descriptive.
589590 if (parts.isEmpty()) return " 0"
590- return parts.joinToString(" _" ) { name ->
591+ return parts.joinToString(" _" ) { slotName ->
592+ // A functional-interface slot (issue #216) is named by its interface, not by the callable form.
593+ val name = slotName.substringBefore(SAM_SEPARATOR )
591594 val chosen = if (qualified) name else name.substringAfterLast(' .' )
592595 chosen.map { if (it.isLetterOrDigit()) it else ' _' }.joinToString(" " )
593596 }
@@ -1759,6 +1762,9 @@ internal object ArtifactScanner {
17591762 val typeName : String? = null ,
17601763 val boundary : BoundaryType ? = null ,
17611764 val declineReason : String? = null ,
1765+ /* * The type a stub shows for the slot when it is not the declared one: a functional
1766+ * interface's `kotlin.FunctionN` (issue #216). */
1767+ val modelType : KotlinTypeModel ? = null ,
17621768 )
17631769
17641770 /* *
@@ -1851,6 +1857,142 @@ internal object ArtifactScanner {
18511857 return FunctionSlot (typeName = name, boundary = BoundaryType (" OBJECT" , " (%s as $rendered )" , " (%s)" ))
18521858 }
18531859
1860+ /* *
1861+ * A parameter typed as a **functional interface** (issue #216): a Java interface with one abstract
1862+ * method (`java.lang.Runnable`) or a Kotlin `fun interface`, whose method returns `Unit` and which is
1863+ * not generic. `null` for anything else, which leaves the parameter exactly as it was -- an
1864+ * interface on the classpath still crosses as a handle (`Alignment` is a `fun interface` whose
1865+ * method returns an `IntOffset`, and its slots must keep taking `Alignment.Center`).
1866+ *
1867+ * The slot takes **both** a Python callable and a Kotlin handle. Its type name is the interface's,
1868+ * then [SAM_SEPARATOR], then the `kotlin.FunctionN` it is equivalent to
1869+ * (`java.lang.Runnable|kotlin.Function0()->kotlin.Unit`): the binding layer fills it from a callable
1870+ * through the wrapper path a function type has (B-5, `pythonx._make_function`) and checks a handle
1871+ * against the interface. The generated read converts a wrapper with a SAM lambda and casts anything
1872+ * else -- `((%s).let { v -> if (v is kotlin.Function0<*>) java.lang.Runnable { ... } else v as
1873+ * java.lang.Runnable })` -- which `kotlinc` compiles; nothing is looked up at run time (AGENTS.md §12).
1874+ *
1875+ * The interface is read off the consumer's classpath, or, for a `java.`/`javax.` one, off the JDK
1876+ * this build runs on ([jdkClassNode]): the walk's classpath is the consumer's jars and never held
1877+ * `java.lang.Runnable`, which is why `runRunnable(block: Runnable)` was declined.
1878+ */
1879+ private fun functionalInterfaceSlotOrNull (type : KmType , classpath : ArtifactClasspath ): FunctionSlot ? {
1880+ val classifier = type.classifier as ? KmClassifier .Class ? : return null
1881+ val internal = classifier.name
1882+ if (internal.startsWith(KOTLIN_FUNCTION_INTERNAL_PREFIX )) return null
1883+ if (type.arguments.isNotEmpty()) return null
1884+ val binary = internal.replace(' .' , ' $' )
1885+ val node = classpath.classNode(binary) ? : jdkClassNode(binary) ? : return null
1886+ if (! node.access.hasFlag(Opcodes .ACC_INTERFACE ) || ! node.access.hasFlag(Opcodes .ACC_PUBLIC )) return null
1887+ if (node.signature?.startsWith(" <" ) == true ) return null
1888+ val kmClass = when (val metadata = kotlinClassMetadataOf(node)) {
1889+ null -> null
1890+ is KotlinClassMetadata .Class -> metadata.kmClass.takeIf { it.isFunInterface } ? : return null
1891+ else -> return null
1892+ }
1893+ val method = abstractMethodsOf(node, classpath)?.singleOrNull() ? : return null
1894+ val (descriptors, returns) = splitMethodDescriptor(method.desc)
1895+ if (returns != " V" ) return null
1896+ // A Kotlin `fun interface` says each parameter's nullability; a Java one does not, so an object
1897+ // parameter is taken as nullable there -- Java may pass `null`, and a non-null type would throw.
1898+ val parameterTypes: List <Pair <String , String >> = if (kmClass != null ) {
1899+ val declared = kmClass.functions.singleOrNull { it.name == method.name } ? : return null
1900+ if (declared.valueParameters.size != descriptors.size) return null
1901+ declared.valueParameters.map { parameter ->
1902+ val name = kotlinClassifierNameOf(parameter.type) ? : return null
1903+ name to (renderKotlinSourceType(parameter.type, classpath, depth = 0 ) ? : return null )
1904+ }
1905+ } else {
1906+ descriptors.map { descriptor -> javaParameterTypeOf(descriptor, classpath) ? : return null }
1907+ }
1908+ val qualified = internal.replace(' /' , ' .' )
1909+ val arity = parameterTypes.size
1910+ val functionType = " kotlin.Function$arity <" + (parameterTypes.map { it.second } + UNIT_TYPE_NAME ).joinToString(" , " ) + " >"
1911+ val star = " kotlin.Function$arity <" + List (arity + 1 ) { " *" }.joinToString(" , " ) + " >"
1912+ val names = (0 until arity).map { " p$it " }
1913+ val arrow = if (arity == 0 ) " " else names.joinToString(" , " , postfix = " -> " )
1914+ val convert = " v -> if (v is $star ) $qualified { $arrow (v as $functionType )(${names.joinToString(" , " )} ) } else v as $qualified "
1915+ val read = if (type.isNullable) " ((%s)?.let { $convert })" else " ((%s).let { $convert })"
1916+ return FunctionSlot (
1917+ typeName = qualified + SAM_SEPARATOR + KOTLIN_FUNCTION_PREFIX + arity +
1918+ " (" + parameterTypes.joinToString(" ," ) { it.first } + " )" + FUNCTION_RETURNS + UNIT_TYPE_NAME ,
1919+ boundary = BoundaryType (" OBJECT" , read, " (%s)" ),
1920+ modelType = KotlinTypeModel (
1921+ " kotlin.Function$arity " ,
1922+ isNullable = type.isNullable,
1923+ arguments = parameterTypes.map { KotlinTypeModel (it.first) } + KotlinTypeModel (UNIT_TYPE_NAME ),
1924+ ),
1925+ )
1926+ }
1927+
1928+ /* * Separates a functional-interface slot's interface from the function type a callable fills it as
1929+ * (issue #216). Must match `pythonx`'s `_SAM_SEPARATOR`; not a character a classifier can contain. */
1930+ internal const val SAM_SEPARATOR : String = " |"
1931+
1932+ /* * `Object`'s public methods, which an interface may redeclare without becoming non-functional. */
1933+ private val OBJECT_METHOD_SIGNATURES = setOf (
1934+ " equals" to " (Ljava/lang/Object;)Z" ,
1935+ " hashCode" to " ()I" ,
1936+ " toString" to " ()Ljava/lang/String;" ,
1937+ )
1938+
1939+ /* *
1940+ * The abstract methods an interface leaves to an implementation, its superinterfaces' included and
1941+ * minus any a subinterface gives a default body; `null` when a superinterface cannot be read.
1942+ */
1943+ private fun abstractMethodsOf (node : ClassNode , classpath : ArtifactClasspath ): List <org.objectweb.asm.tree.MethodNode >? {
1944+ val abstract = LinkedHashMap <Pair <String , String >, org.objectweb.asm.tree.MethodNode > ()
1945+ val concrete = HashSet <Pair <String , String >>()
1946+ fun visit (current : ClassNode , depth : Int ): Boolean {
1947+ if (depth > 8 ) return false
1948+ current.methods.filter { ! it.access.hasFlag(Opcodes .ACC_STATIC ) && ! it.access.hasFlag(Opcodes .ACC_SYNTHETIC ) }.forEach { method ->
1949+ val key = method.name to method.desc
1950+ if (key in OBJECT_METHOD_SIGNATURES || method.name.startsWith(" <" )) return @forEach
1951+ if (method.access.hasFlag(Opcodes .ACC_ABSTRACT )) abstract.putIfAbsent(key, method) else concrete + = key
1952+ }
1953+ return current.interfaces.all { parent ->
1954+ val parentNode = classpath.classNode(parent) ? : jdkClassNode(parent) ? : return false
1955+ visit(parentNode, depth + 1 )
1956+ }
1957+ }
1958+ if (! visit(node, 0 )) return null
1959+ return abstract.filterKeys { it !in concrete }.values.toList()
1960+ }
1961+
1962+ /* *
1963+ * A Java method parameter as `(slot type name, Kotlin source type)`: primitives and `String` by
1964+ * their Kotlin names, any other class by its name when it is nameable, an object type nullable.
1965+ */
1966+ private fun javaParameterTypeOf (descriptor : String , classpath : ArtifactClasspath ): Pair <String , String >? {
1967+ val primitive = when (descriptor) {
1968+ " I" -> " kotlin.Int" ; " J" -> " kotlin.Long" ; " Z" -> " kotlin.Boolean" ; " D" -> " kotlin.Double"
1969+ " F" -> " kotlin.Float" ; " S" -> " kotlin.Short" ; " B" -> " kotlin.Byte" ; " C" -> " kotlin.Char"
1970+ else -> null
1971+ }
1972+ if (primitive != null ) return primitive to primitive
1973+ if (! descriptor.startsWith(" L" ) || ! descriptor.endsWith(" ;" )) return null
1974+ val binary = descriptor.substring(1 , descriptor.length - 1 )
1975+ val name = when (binary) {
1976+ " java/lang/String" -> " kotlin.String"
1977+ " java/lang/Object" -> " kotlin.Any"
1978+ else -> if (classpath.isNameablePublicClass(binary)) binary.replace(' /' , ' .' ).replace(' $' , ' .' ) else return null
1979+ }
1980+ return name to " $name ?"
1981+ }
1982+
1983+ /* *
1984+ * A `java.`/`javax.` class file, read from the JDK this build runs on -- the walk's classpath is the
1985+ * consumer's jars, which never contain the platform. Build time only, and only to recognise a
1986+ * functional interface ([functionalInterfaceSlotOrNull]); it does not make a JDK type nameable as a
1987+ * handle anywhere else.
1988+ */
1989+ private fun jdkClassNode (binaryName : String ): ClassNode ? {
1990+ if (! binaryName.startsWith(" java/" ) && ! binaryName.startsWith(" javax/" )) return null
1991+ val bytes = ClassLoader .getPlatformClassLoader().getResourceAsStream(" $binaryName .class" )?.use { it.readBytes() }
1992+ ? : return null
1993+ return readClassNodeOrNull(bytes)
1994+ }
1995+
18541996 /* * The Kotlin-source spelling of a function type: `kotlin.Function1<kotlin.Float, kotlin.Unit>`.
18551997 * Written as the `FunctionN` classifier rather than as `(Float) -> Unit` because the two are the
18561998 * same type and the first needs no bracket rules; assignability to an extension function type
@@ -2126,7 +2268,7 @@ internal object ArtifactScanner {
21262268 // Read before `resolveKotlinType` is asked anything, because for a function-typed parameter
21272269 // it is the one that would answer -- and would answer "no". See [functionSlotOrNull].
21282270 val functionSlots = function.allParameterTypes.mapIndexed { index, type ->
2129- functionSlotOrNull(type, paramDescriptors[index], classpath)
2271+ functionSlotOrNull(type, paramDescriptors[index], classpath) ? : functionalInterfaceSlotOrNull(type, classpath)
21302272 }
21312273 functionSlots.firstNotNullOfOrNull { it?.declineReason }?.let { return declined(it) }
21322274
@@ -2207,13 +2349,16 @@ internal object ArtifactScanner {
22072349 // A member's defaults are not omittable yet (issue #215): no plan, so Python passes every
22082350 // argument and the stub says so.
22092351 val omittable = if (member != null ) emptySet() else omittableParameterIndices(function, receiverIndex)
2352+ val modelParameters = model.parameters.mapIndexed { index, parameter ->
2353+ functionSlots[index + receiverIndex]?.modelType?.let { parameter.copy(type = it) } ? : parameter
2354+ }
22102355 return Candidate (
22112356 callable = callable,
22122357 declaration = model.copy(
22132358 bindingName = qualifiedName,
22142359 returnBoundaryTag = returnType.tag,
22152360 receiverBoundaryTag = if (function.isExtension) resolvedParams.first().tag else model.receiverBoundaryTag,
2216- parameters = model.parameters .mapIndexed { index, parameter ->
2361+ parameters = modelParameters .mapIndexed { index, parameter ->
22172362 parameter.copy(boundaryTag = resolvedParams[index + receiverIndex].tag)
22182363 },
22192364 ),
0 commit comments