-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Compiler plugin for JVM partially works (still needs META-INF) genera…
…tion
- Loading branch information
Showing
11 changed files
with
292 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
} |
13 changes: 13 additions & 0 deletions
13
sweetspi-compiler-plugin/src/main/kotlin/SweetCommandLineProcessor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
21 changes: 21 additions & 0 deletions
21
sweetspi-compiler-plugin/src/main/kotlin/SweetCompilerPluginRegistrar.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
sweetspi-compiler-plugin/src/main/kotlin/SweetFirCheckers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
|
||
} |
14 changes: 14 additions & 0 deletions
14
sweetspi-compiler-plugin/src/main/kotlin/SweetFirExtensionRegistrar.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
181
sweetspi-compiler-plugin/src/main/kotlin/SweetIrGenerationExtension.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
12 changes: 12 additions & 0 deletions
12
sweetspi-tests/multiplatform/src/commonTest/kotlin/TestService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
18 changes: 18 additions & 0 deletions
18
sweetspi-tests/multiplatform/src/commonTest/kotlin/TestServiceImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
...resources/META-INF/services/dev.whyoleg.sweetspi.tests.multiplatform.TestService_Provider
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |