Skip to content

Commit

Permalink
Compiler plugin for JVM partially works (still needs META-INF) genera…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
whyoleg committed Jan 27, 2025
1 parent a0b5e0d commit f7116d1
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 80 deletions.
13 changes: 13 additions & 0 deletions sweetspi-compiler-plugin/src/main/kotlin/SweetClassIds.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.name.*

object SweetClassIds {
private val pkg = FqName("dev.whyoleg.sweetspi")
val Service = ClassId(pkg, Name.identifier("Service"))
val ServiceProvider = ClassId(pkg, Name.identifier("ServiceProvider"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.compiler.plugin.*

@OptIn(ExperimentalCompilerApi::class)
class SweetCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = "dev.whyoleg.sweetspi"
override val pluginOptions: Collection<CliOption> = emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.backend.common.extensions.*
import org.jetbrains.kotlin.compiler.plugin.*
import org.jetbrains.kotlin.config.*
import org.jetbrains.kotlin.fir.extensions.*
import org.jetbrains.kotlin.ir.util.*

@OptIn(ExperimentalCompilerApi::class)
class SweetCompilerPluginRegistrar : CompilerPluginRegistrar() {
override val supportsK2: Boolean get() = true

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
FirExtensionRegistrarAdapter.registerExtension(SweetFirExtensionRegistrar())
IrGenerationExtension.registerExtension(SweetIrGenerationExtension(configuration.irMessageLogger))
}
}
13 changes: 13 additions & 0 deletions sweetspi-compiler-plugin/src/main/kotlin/SweetFirCheckers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.analysis.extensions.*

// checkers that annotations are applied to correct functions/properties/classes
class SweetFirCheckers(session: FirSession) : FirAdditionalCheckersExtension(session) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.fir.extensions.*

class SweetFirExtensionRegistrar : FirExtensionRegistrar() {
override fun ExtensionRegistrarContext.configurePlugin() {
+::SweetFirCheckers
// maybe something else FIR related?
}
}
181 changes: 181 additions & 0 deletions sweetspi-compiler-plugin/src/main/kotlin/SweetIrGenerationExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.compiler

import org.jetbrains.kotlin.backend.common.extensions.*
import org.jetbrains.kotlin.backend.common.lower.*
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.platform.jvm.*

private val SweetOrigin: IrDeclarationOrigin = IrDeclarationOriginImpl("SWEET_SPI")

// here should be:
// - find all @Service/@ServiceProvider/@JvmService/@JvmServiceProvider on class-likes
// - for JVM:
// - (step1) for `@Service` - generate an additional interface (@PublishedApi internal) (FIR + IR)
// - (step1) for `@ServiceProvider` - generate an additional interface impl and meta-inf (FIR + IR + RESOURCES)
// - (step2) for `ServiceLoader.load` - intrinsic for R8 optimizable (IR)
// - (step3) for `@JvmService` - do nothing
// - (step3) for `@JvmServiceProvider` - generate meta-inf (RESOURCES)
// - (step1) for klib - generate init with `@EagerInitializer` (FIR + IR)

// klib:
// - if annotated -> generate call
// jvm:
// - if annotated -> generate a lot of different things :)

@OptIn(UnsafeDuringIrConstructionAPI::class)
class SweetIrGenerationExtension(
private val logger: IrMessageLogger,
) : IrGenerationExtension {

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
if (!pluginContext.platform.isJvm()) return
// generate based on service/serviceProvider based on platform

// TODO: lazy, needed for jvm only
// create custom pluginContext?
val publishedApiAnnotation by lazy {
pluginContext.referenceConstructors(StandardClassIds.Annotations.PublishedApi).single()
}

// handle @Service
val services = moduleFragment.files.flatMap { file ->
file.declarations.mapNotNull { declaration ->
if (declaration is IrClass && declaration.hasAnnotation(SweetClassIds.Service)) {
buildJvmServiceProviderClass(pluginContext, declaration, publishedApiAnnotation)
} else null
}.onEach(file::addChild)
}.associateBy(IrClass::classIdOrFail) // should be called ONLY after `addChild`

// handle @ServiceProvider
moduleFragment.files.forEach { file ->
file.declarations.flatMap { declaration ->
val serviceTypes = findDeclaredServiceTypes(declaration)
?.ifEmpty { resolveServiceTypes(declaration) }
?: return@flatMap emptyList()

// messageCollector.report(
// CompilerMessageSeverity.WARNING,
// "$declaration: ${serviceTypes.joinToString { it.classFqName!!.asString() }}"
// )

// TODO: add checkers for invalid combinations
when (declaration) {
is IrClass -> buildJvmServiceProviderImplClasses(
pluginContext,
services,
declaration.name,
serviceTypes
) {
if (declaration.isObject) irGetObject(declaration.symbol)
else irCall(declaration.constructors.single {
it.valueParameters.isEmpty() // TODO?
})
}
is IrSimpleFunction -> buildJvmServiceProviderImplClasses(
pluginContext,
services,
declaration.name,
serviceTypes
) {
irCall(declaration) // should have no arguments
}
is IrProperty -> buildJvmServiceProviderImplClasses(
pluginContext,
services,
declaration.name,
serviceTypes
) {
irCall(declaration.getter!!)
}
else -> emptyList()
}
}.forEach(file::addChild)
}
}

private fun buildJvmServiceProviderClass(
pluginContext: IrPluginContext,
declaration: IrClass,
publishedApiAnnotation: IrConstructorSymbol,
): IrClass = pluginContext.irFactory.buildClass {
origin = SweetOrigin
kind = ClassKind.INTERFACE
modality = Modality.ABSTRACT
visibility = DescriptorVisibilities.INTERNAL
name = Name.identifier("${declaration.name.identifier}_Provider")
}.apply {
createParameterDeclarations()
superTypes += pluginContext.irBuiltIns.functionN(0).typeWith(declaration.defaultType)
annotations += IrConstructorCallImpl.fromSymbolOwner(publishedApiAnnotation.owner.returnType, publishedApiAnnotation)
}

private fun buildJvmServiceProviderImplClasses(
pluginContext: IrPluginContext,
services: Map<ClassId, IrClass>,
declarationName: Name,
serviceTypes: List<IrType>,
serviceExpression: IrBlockBodyBuilder.() -> IrExpression,
): List<IrClass> = serviceTypes.map { serviceType ->
pluginContext.irFactory.buildClass {
origin = SweetOrigin
kind = ClassKind.CLASS
visibility = DescriptorVisibilities.INTERNAL
name = Name.identifier("${declarationName.identifier}_Provider")
}.apply {
createParameterDeclarations()
val serviceId = ClassId.topLevel(FqName(serviceType.classFqName!!.asString() + "_Provider"))
superTypes += (services[serviceId]?.symbol ?: pluginContext.referenceClass(serviceId)
?: error("TBD: $serviceId")).defaultType
// empty constructor
addConstructor { isPrimary = true }.apply {
val constructor = pluginContext.irBuiltIns.anyClass.owner.constructors.single()
body = DeclarationIrBuilder(pluginContext, symbol).irBlockBody(startOffset, endOffset) {
+irDelegatingConstructorCall(constructor)
}
}
addFunction {
name = Name.identifier("invoke")
returnType = pluginContext.irBuiltIns.anyClass.defaultType // any because of generic
}.apply {
dispatchReceiverParameter = parentAsClass.thisReceiver!!.copyTo(this)
body = DeclarationIrBuilder(pluginContext, symbol).irBlockBody(startOffset, endOffset) {
+irReturn(serviceExpression())
}
}
}
}

private fun findDeclaredServiceTypes(declaration: IrDeclaration): List<IrType>? {
val serviceProviderAnnotation = declaration.annotations.find {
it.symbol.owner.parentAsClass.classId == SweetClassIds.ServiceProvider
} ?: return null

@Suppress("UNCHECKED_CAST")
return ((serviceProviderAnnotation.getValueArgument(0) as IrVararg).elements as List<IrClassReference>).map { it.classType }
}

private fun resolveServiceTypes(declaration: IrDeclaration): List<IrType> {
val rootType = when (declaration) {
is IrClass -> declaration
is IrSimpleFunction -> declaration.returnType.classOrFail.owner
is IrProperty -> declaration.getter!!.returnType.classOrFail.owner
else -> error("TBD: ${declaration::class.simpleName}")
}
return (rootType.getAllSuperclasses() + rootType).filter {
it.hasAnnotation(SweetClassIds.Service)
}.map(IrClass::defaultType)
}
}
64 changes: 0 additions & 64 deletions sweetspi-compiler-plugin/src/main/kotlin/entrypoint.kt

This file was deleted.

12 changes: 12 additions & 0 deletions sweetspi-tests/multiplatform/src/commonTest/kotlin/TestService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.tests.multiplatform

import dev.whyoleg.sweetspi.*

@Service
interface TestService {
fun value(): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.sweetspi.tests.multiplatform

import dev.whyoleg.sweetspi.*

@ServiceProvider
object TestServiceImpl : TestService {
override fun value(): String = "object"
}

@ServiceProvider
fun testServiceImplFunction(): TestService = TestServiceImpl

@ServiceProvider
val testServiceImplProperty: TestService get() = TestServiceImpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,6 @@ package dev.whyoleg.sweetspi.tests.multiplatform
import dev.whyoleg.sweetspi.*
import kotlin.test.*

@Service
interface TestService {
fun value(): String
}

@ServiceProvider
object TestServiceImpl : TestService {
override fun value(): String = "object"
}

@ServiceProvider
fun testServiceImplFunction(): TestService = TestServiceImpl

@ServiceProvider
val testServiceImplProperty: TestService get() = TestServiceImpl

class TestServiceTest {
@Test
fun test() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
#

dev.whyoleg.sweetspi.tests.multiplatform.TestServiceImpl_Provider
dev.whyoleg.sweetspi.tests.multiplatform.testServiceImplFunction_Provider
dev.whyoleg.sweetspi.tests.multiplatform.testServiceImplProperty_Provider

0 comments on commit f7116d1

Please sign in to comment.