Writing a Kotlin Compiler Plugin with Arrow Meta

Kotlin Compiler

https://meta.arrow-kt.io/

Use Case

fun prime(n: Int): Long = listOf(1L,2L,3L).take(n).last()
annotation class DebugLog@DebugLog
fun prime(n: Int): Long = listOf(1L,2L,3L).take(n).last()
fun prime(n: Int): Long {
println(“-> prime(n=$n)”)
val startTime = System.currentTimeMillis()
val result = listOf(1L,2L,3L).take(n).last()
val timeToRun = System.currentTimeMillis() — startTime
println(“<- prime[ran in $timeToRun ms]”)
return result
}
  • Kotlin Embeddable Compiler
  • Arrow Meta Compiler Plugin
  • Shadow Jar library
plugins {
id “org.jetbrains.kotlin.jvm”
id “com.github.johnrengelman.shadow”
}
dependencies {
compileOnly “org.jetbrains.kotlin:
kotlin-stdlib:$KOTLIN_VERSION”
compileOnly “org.jetbrains.kotlin:
kotlin-compiler-embeddable:$KOTLIN_VERSION”
compileOnly “com.github.arrow-kt.arrow-meta:
compiler-plugin:-SNAPSHOT”
}shadowJar {
configurations = [project.configurations.compileOnly]
dependencies {
exclude(“org.jetbrains.kotlin:kotlin-stdlib”)
exclude(“org.jetbrains.kotlin:kotlin-compiler-embeddable”)
}
}
compileKotlin {
kotlinOptions {
jvmTarget = “$JVM_TARGET_VERSION”
freeCompilerArgs = [
“-Xplugin=${project.rootDir}/create
plugin/build/libs/create-plugin-all.jar”]
}
}
testImplementation 
‘com.github.arrow-kt.arrow-meta:testing-plugin:-SNAPSHOT’
class DebugLogMetaPlugin : Meta {    @ExperimentalContracts
override fun intercept(ctx: CompilerContext): List<Plugin> =
listOf(
debugLog
)
}
val Meta.debugLog: Plugin
get() =
"DebugLog" {
meta
(
namedFunction({ validateFunction() }) { c: KtNamedFunction ->
Transform.replace(
replacing = c,
newDeclaration = replace(c).function
)
}
)
}
data class Plugin(  
val name: String,
val meta: CompilerContext.() -> List<ExtensionPhase>
)
fun Meta.namedFunction(
match: KtNamedFunction.() -> Boolean,
map: NamedFunction.(KtNamedFunction) -> Transform<KtNamedFunction>
): ExtensionPhase =
quote(match, map) { NamedFunction(it) }
@DebugLog
fun prime(n: Int): Long
/**
* Match a function that fits this scenario.
*
* - Function has 1 param that is of Int type.
* - Function has a Long return type.
* - Function has a DebugLog annotation.
*/
private fun KtNamedFunction.validateFunction(): Boolean =
hasOneIntParam() &&
hasLongReturnType() &&
hasAnnotation(DEBUG_LOG)
/**
* Check function has 1 param that is of Int type.
*/
private fun KtNamedFunction.hasOneIntParam(): Boolean =
valueParameterList?.parameters?.size == 1 &&
valueParameterList?.parameters?.first()
?.typeReference?.text == INT

/**
* Check function is returning a Long.
*/
private fun KtNamedFunction.hasLongReturnType(): Boolean =
hasDeclaredReturnType() && typeReference?.text == LONG
/**
* Check function has [annotationNames] as an annotation.
*/
fun KtAnnotated.hasAnnotation(
vararg annotationNames: String
): Boolean {
val names = annotationNames.toHashSet()
val predicate: (KtAnnotationEntry) -> Boolean = {
it.typeReference
?.typeElement
?.safeAs<KtUserType>()
?.referencedName in names
}
return annotationEntries.any(predicate)
}
fun prime(n: Int): Long {
println(“-> prime(n=$n)”)
val startTime = System.currentTimeMillis()
val result = listOf(1L,2L,3L).take(n).last()
val timeToRun = System.currentTimeMillis() — startTime
println(“<- prime[ran in $timeToRun ms]”)
return result
}
fun replace(function: KtNamedFunction): String {
val functionName = function.name
val paramName = function.valueParameters.first().name
val functionBody = function.body()?.bodySourceAsExpression()

return """
|//metadebug
|
| fun ${functionName}(${paramName}: Int): Long {
| println("-> $functionName(${paramName}=$${paramName})")
| val startTime = System.currentTimeMillis()
| val result = $functionBody
| val timeToRun = System.currentTimeMillis() - startTime
| println("<- ${functionName}[ran in ${'$'}timeToRun ms]")
| return result
| }"""
}
val Meta.debugLog: Plugin
get() =
"DebugLog" {
meta(
namedFunction({ validateFunction() }) { c: KtNamedFunction ->
Transform.replace(
replacing = c,
newDeclaration = replace(c).function
)

}
)
}

Usage & Output

@DebugLog
fun prime(n: Int): Long = listOf(1L,2L,3L).take(n).last()

fun main() {
prime(10)
}
-> prime(n=10)
<- prime[ran in 36 ms]

Testing

class DebugLogMetaPluginTest {

companion object {

val debuglog = """
|annotation class DebugLog
|
|@DebugLog
|fun prime(n: Int): Long =
| listOf(1L,2L,3L).take(n).last()
| """.trimMargin().trim().source

val expectedOutput = """
|annotation class DebugLog
|
|//metadebug
|fun prime(n: Int): Long {
| println("-> prime(n=${'$'}n)")
| val startTime = System.currentTimeMillis()
| val result = listOf(1L,2L,3L).take(n).last()
| val timeToRun = System.currentTimeMillis() - startTime
| println("<- prime[ran in ${'$'}timeToRun ms]")
| return result
| }""".trimMargin().trim().source
}
}
@Test
fun `should print function time execution`() {
assertThis(CompilerTest(
config = { listOf(addMetaPlugins(DebugLogMetaPlugin())) },
code = { debuglog },
assert = { quoteOutputMatches(expectedOutput) }
))
}
Replacing class arrow.meta.internal.kastree.ast.Node$Decl$Func with [class org.jetbrains.kotlin.psi.KtNamedFunction]: newContents: 
//metadebug
fun prime(n: Int): Long {
println("-> prime(n=$n)")
val startTime = System.currentTimeMillis()
val result = listOf(1L,2L,3L).take(n).last()
val timeToRun = System.currentTimeMillis() - startTime
println("<- prime[ran in $timeToRun ms]")
return result
}
Transformed file: KtFile: Source.kt. New contents:
[(KtFile: Source.kt, annotation class DebugLog
//metadebug
fun prime(n: Int): Long {
println("-> prime(n=$n)")
val startTime = System.currentTimeMillis()
val result = listOf(1L,2L,3L).take(n).last()
val timeToRun = System.currentTimeMillis() - startTime
println("<- prime[ran in $timeToRun ms]")
return result
}
)]
END quote.doAnalysis: [KtFile: Source.kt]

Conclusion

Resources

--

--

--

Kotlin Advocate & Android Engineer

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

How Do Linkers Work

How is hash collision handled in HashMap?

Cloud Computing Roles

What does Cloud Native mean? Let’s discuss Containers, Orchestration, and Microservices

Boris Johnson to outline Christmas Covid rules

Global Day of Code & Conway’s Game of Life

Quill- Most efficient Scala driver for Apache Cassandra and Spark

3 Necessarily VS Code extensions for developers

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mohit Sarveiya

Mohit Sarveiya

Kotlin Advocate & Android Engineer

More from Medium

An Intermediate Guide to Dagger

Espresso UI testing made easy with kotlin extensions

Stackless Coroutine, Stackfulness Coroutine 區別簡單說明

Kotlin for Android Development(Part-2)