Skip to content

Commit

Permalink
feat!: generate library code for field operations on kotlin standard …
Browse files Browse the repository at this point in the history
…library types (#273)

* build: add code-generator module

* feat: support runtime alignement check for an arbitrary field count

* feat: add shortcut for getting to the set of neighbors from a field

* porcaio

* stash this commit

* feat: add combinator

* some minor

* some minor

* chore: updates

* feat: add primitives code generator

* feat: support the generation for property members

* feat: generate a file for each specified type

* feat: provide a dedicate function for code generation

* test: provide minimal test for validating the generated code

* refactor: use KClass instead hard-coded string for collektive classes

* refactor: move Utils into codgen folder

* docs: improve kdoc of Field

* chore(deps): move dependency into catalog

* build: sort dependencies

* fix: do not generate field-based method for internal member

* fix: include also the receiver in the checkAligned method

* refactor: move into buildSrc the code generation

* feat: implement field primitives code generator inside buildSrc

* feat: introduce stdlib project

* build: remove hard-coded Field class and use source set pointing to dsl project

* docs: fix documentation comment

* build: enable compiler plugin

* build: enable the foojay resolver in buildSrc

* build: enable the multi jvm test in buildSrc

* feat(stdlib): add a header to the generated sources

* style(codegen): improve readability

* refactor(codegen): generate code with Java 8 and improve the namespacing

* fixup: work around the limitations with recurring generic types

* feat: simplify the type resolution

* fix: exclude `associate`(`By`)`To`

* chore: ignore .kotlin

* fix: exclude `associateWithTo`

* fix: exclude `joinTo`

* fix: exclude `filterIndexedTo`

* fix: exclude `filterNotTo`

* fix: exclude `filterTo`

* fix: exclude `binarySearch`

* fix: exclude `map` and `mapTo`

* fix: exclude `mapNotNullTo`

* fix: exclude `mapIndexedTo`

* fix: exclude `groupByTo`

* fix: exclude `flatMapTo`

* fix: exclude `mapIndexedNotNullTo`

* fix(codegen): add star projection support

* fix(codegen): retain `inline` and `infix`

* feat(codegen): add support for `reified` type parameters in method signatures

* fix(codegen): add support for `crossinline` function types

* fix: solved error No type arguments expected for class *Array

* feat(stdlib): split the generated code into multiple files based on the receiver type

* feat(stdlib): generate type arguments for 0-ary functions

* feat(stdlib): improve wildcard detection and rename

* feat(stdlib): skip filterIsInstanceTo

* feat(stdlib): skip filterNotNullTo

* feat(stdlib): detect nullables in jvmnames

* feat(stdlib): exclude generation for functions affected by square/kotlinpoet#1933

* feat(stdlib): make the generated code compile

* fix(codegen): work around square/kotlinpoet#1933

* feat(codegen)!: avoid type variable names as package segments

* refactor: code refactoring in appropriate utils objects

---------

Co-authored-by: Danilo Pianini <[email protected]>
Co-authored-by: Danilo Pianini <[email protected]>
Co-authored-by: Angela Cortecchia <[email protected]>
  • Loading branch information
4 people authored Jul 2, 2024
1 parent dc8d650 commit 31545f2
Show file tree
Hide file tree
Showing 14 changed files with 813 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
.gradle/
build/
.idea/
.kotlin/
.vscode
bin
build/
kotlin-js-store
*.hprof
local.properties
Expand Down
10 changes: 10 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
`kotlin-dsl`
id("org.danilopianini.multi-jvm-test-plugin") version "0.5.8"
}

repositories {
Expand All @@ -9,6 +10,15 @@ repositories {

with(extensions.getByType<VersionCatalogsExtension>().named("libs")) {
dependencies {
implementation(kotlin("reflect"))
implementation(findLibrary("kotlin-gradle-plugin").get())
implementation(findLibrary("kotlinpoet").get())
implementation(findLibrary("arrow").get())
}
}

sourceSets {
main {
kotlin.srcDir("../dsl/src/commonMain")
}
}
2 changes: 2 additions & 0 deletions buildSrc/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
apply(plugin = "org.gradle.toolchains.foojay-resolver-convention")

dependencyResolutionManagement {
versionCatalogs {
create("libs") {
Expand Down
8 changes: 8 additions & 0 deletions buildSrc/src/main/kotlin/KotlinPoetBug.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import com.squareup.kotlinpoet.asTypeName
import kotlin.reflect.jvm.kotlinFunction

fun main(args: Array<String>) {
Class.forName("kotlin.collections.ArraysKt").methods.first {
it.name == "max" && it.parameters.first().parameterizedType.typeName.contains("Comparable")
}.kotlinFunction?.parameters?.first()?.type?.asTypeName()
}
25 changes: 25 additions & 0 deletions buildSrc/src/main/kotlin/KotlinPoetBug2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName

fun <T> foo(): Array<out T> = TODO()

fun main() {
val funspec = FunSpec.builder("foo")
.addTypeVariable(TypeVariableName("T"))
.returns(
ClassName.bestGuess("kotlin.Array")
.parameterizedBy(WildcardTypeName.producerOf(TypeVariableName("T")))
)
.addStatement("TODO()")
.build()
println(
FileSpec.builder("foo.bar", "Baz")
.addFunction(funspec)
.build()
.toString()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package it.unibo.collektive.codegen

import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.toolchain.JavaLanguageVersion
import java.io.File
import java.net.URLClassLoader

abstract class CollektiveCodegenTask : JavaExec() {
@OutputDirectory
lateinit var outputDir: File

init {
this.javaLauncher.set(
javaToolchainService.launcherFor {
languageVersion.set(JavaLanguageVersion.of(8))
}
)
mainClass.set(FieldedMembersGenerator::class.qualifiedName)
}

@TaskAction
override fun exec() {
args(outputDir.absolutePath)
val classpath: Array<String> = sequenceOf(this::class.java.classLoader)
//generateSequence(this::class.java.classLoader) { it.parent }
.filterIsInstance<URLClassLoader>()
.flatMap { it.urLs.asSequence() }
.orEmpty()
.mapNotNull { it.file.takeUnless { it.isNullOrBlank() } }
.toList()
.toTypedArray()
check(classpath.isNotEmpty()) {
"Classpath detection for the Collektive code generation task failed."
}
classpath(*classpath)
outputDir.mkdirs()
super.exec()
}

companion object {
@JvmStatic
fun main(args: Array<String>) {
val outputDir = args[0]
FieldedMembersGenerator.generateFieldFunctionsForTypes(FieldedMembersGenerator.baseTargetTypes).forEach {
it.writeTo(File(outputDir))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package it.unibo.collektive.codegen

import com.squareup.kotlinpoet.FileSpec
import it.unibo.collektive.codegen.utils.generatePrimitivesFile
import it.unibo.collektive.field.Field
import java.io.File
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.KTypeParameter
import kotlin.reflect.KVisibility
import kotlin.reflect.full.extensionReceiverParameter
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.jvm.kotlinFunction
import kotlin.reflect.typeOf


object FieldedMembersGenerator {

val baseExtensions = sequenceOf(
"Arrays", "Collections", "Sets", "Maps"
).map {
Class.forName("kotlin.collections.${it}Kt").kotlin
}

/**
* The base types for which to generate field functions.
*/
val baseTargetTypes = sequenceOf(
// Boolean
Boolean::class,
// Numeric types
Byte::class,
Short::class,
Int::class,
Float::class,
Double::class,
Long::class,
// Unsigned types
UByte::class,
UShort::class,
UInt::class,
ULong::class,
// Chars and strings
Char::class,
CharSequence::class,
String::class,
// Collections
Collection::class,
Iterable::class,
List::class,
Map::class,
Sequence::class,
Set::class,
// Other types
Comparator::class,
Pair::class,
Triple::class,
Result::class,
// Function collections
) + baseExtensions

val mutablesAndSideEffects: List<KType> = listOf(
typeOf<Array<*>>(),
typeOf<MutableCollection<*>>(),
typeOf<MutableCollection<*>>(),
typeOf<MutableIterable<*>>(),
typeOf<MutableIterator<*>>(),
typeOf<MutableList<*>>(),
typeOf<MutableListIterator<*>>(),
typeOf<MutableMap<*, *>>(),
typeOf<ShortArray>(),
typeOf<IntArray>(),
typeOf<FloatArray>(),
typeOf<DoubleArray>(),
typeOf<LongArray>(),
typeOf<MutableSet<*>>(),
typeOf<Unit>(),
)

fun KType.isMutable(): Boolean = mutablesAndSideEffects.any { isSubtypeOf(it) }

fun KTypeParameter.isMutable() = upperBounds.any { bound -> bound.isMutable() }

val forbiddenPrefixes = listOf(
"java",
"javax",
)

val permanentlyExcludedMemberNames = listOf(
"associateByTo",
"associateTo",
"associateWithTo",
"binarySearch",
"clone",
"copyInto",
"dec",
"filterIndexedTo",
"filterIsInstanceTo",
"filterNotNullTo",
"filterNotTo",
"filterTo",
"flatMapTo",
"fold",
"foldIndexed",
"foldIndexedOrNull",
"foldLeft",
"foldLeftIndexed",
"foldOrNull",
"foldRight",
"foldRightIndexed",
"groupByTo",
"inc",
"joinTo",
"listOf",
"listOfNotNull",
"map",
"mapIndexedNotNullTo",
"mapIndexedTo",
"mapNotNullTo",
"mapOf",
"mapTo",
"reduce",
"reduceIndexed",
"reduceIndexedOrNull",
"reduceLeft",
"reduceLeftIndexed",
"reduceOrNull",
"reduceRight",
"reduceRightIndexed",
"readObject",
"setOf",
"setOfNotNull",
"toBooleanArray",
"toByteArray",
"toCharArray",
"toCollection",
"toDoubleArray",
"toFloatArray",
"toIntArray",
"toLongArray",
"toShortArray",
"toMutableMap",
"toMutableList",
"toMutableSet",
"toTypedArray",
"writeObject",
) + listOf("fold", "reduce")
.flatMap { listOf(it, "${it}Indexed", "${it}Left", "${it}Right", "${it}OrNull") }
.flatMap { listOf(it, "${it}OrNull", "${it}Indexed") }
.flatMap { listOf(it, "${it}OrNull") }

/**
* Generates field-based functions for the given types.
*/
fun generateFieldFunctionsForTypes(
types: Sequence<KClass<*>>,
excludeMembers: List<String> = emptyList(),
): Sequence<FileSpec> {
fun KCallable<*>.paramTypes() = parameters.drop(1).map { it.type }
val forbiddenMembers = Field::class.members.map { member -> member.name to member.paramTypes() }
// "dec" and "inc" are excluded due to: KT-24800
val forbiddenMembersName = permanentlyExcludedMemberNames + excludeMembers
return types.flatMap { clazz ->
val javaMethods: Collection<KCallable<*>> = clazz.java.methods.mapNotNull {
runCatching { it.kotlinFunction }.getOrNull()
}
val kotlinMembers: Collection<KCallable<*>> = clazz.members
val allTargets = javaMethods.toSet() + kotlinMembers.toSet()
val order = compareBy<KCallable<*>> { it.name }
.thenBy { it.parameters.size }
.thenBy { it.paramTypes().joinToString() }
val sortedTargets = allTargets.sortedWith(order)
val withValidAnnotations = sortedTargets.filter { callable ->
callable.annotations.none {
it is Deprecated || it.annotationClass.simpleName == "PlatformDependent"
}
}
val public = withValidAnnotations.filter { it.visibility == KVisibility.PUBLIC }
val returnTypeMeaningful = public.filterNot {
forbiddenPrefixes.any { prefix -> it.returnType.toString().startsWith(prefix) } ||
mutablesAndSideEffects.any { mutable -> it.returnType.isSubtypeOf(mutable) }
}
val parametersMeaningful = returnTypeMeaningful.filterNot { method ->
method.parameters.any { parameter ->
forbiddenPrefixes.any { prefix -> parameter.type.toString().startsWith(prefix) } ||
when (val classifier = parameter.type.classifier) {
null -> error("Null classifier in $method")
is KClass<*> -> parameter.type.isMutable()
is KTypeParameter -> classifier.isMutable()
else -> error("Unknown classifier type ${classifier::class}")
}
}
}
val genericBoundsMeaningful = parametersMeaningful.filterNot { method ->
method.typeParameters.any { typeParameter ->
forbiddenPrefixes.any { prefix ->
typeParameter.upperBounds.any { it.toString().startsWith(prefix) }
}
}
}
val noConflictingMethods = genericBoundsMeaningful.filterNot { callable ->
callable.name to callable.paramTypes() in forbiddenMembers
}
val validMembers = noConflictingMethods
.filterNot { it.name in forbiddenMembersName }
.toList()
val name = checkNotNull(clazz.simpleName) {
"Cannot generate field functions for anonymous class $clazz"
}.removeSuffix("Kt")
val extensions: Map<String, List<KCallable<*>>> = validMembers.groupBy { callable ->
fun KType?.cleanString(): String = toString().substringBefore('<').substringAfterLast('.')
fun KTypeParameter.allUpperBounds(): List<KType> = upperBounds
.flatMap { (it.classifier as? KTypeParameter)?.allUpperBounds() ?: listOf(it) }
val receiver = callable.extensionReceiverParameter?.type
when (val classifier = receiver?.classifier) {
null -> name
is KTypeParameter -> classifier.allUpperBounds()
.map { it.cleanString() }
.sorted()
.joinToString(separator = "")
else -> receiver.cleanString()
}
}
extensions.asSequence()
.filter { (_, members) -> members.isNotEmpty() }
.mapNotNull { (receiver, members) ->
generatePrimitivesFile(
members,
"it.unibo.collektive.stdlib.${receiver.lowercase()}s",
"Fielded${name}${if (name.endsWith("s")) "Extensions" else 's'}",
)
}
}
}

@JvmStatic
fun main(args: Array<String>) {
val outputDir = if (args.isEmpty()) null else args[0]
generateFieldFunctionsForTypes(baseTargetTypes).forEach { source ->
when {
outputDir == null -> println(source)
else -> source.writeTo(File(outputDir))
}
}
}
}
Loading

0 comments on commit 31545f2

Please sign in to comment.