diff --git a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt index 7082a45..aa3359d 100644 --- a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt +++ b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt @@ -51,9 +51,10 @@ class MainActivity : ComponentActivity() { sendToastOfUserChanges(user) setName(user.name) setSecondName(user.secondName) + printUsernameWithSingletonDistinct(user.name) } - @DistinctUntilChangeFun(false) + @DistinctUntilChangeFun(true) private fun setName(name: String) { logger.log(Level.INFO, "setName: $name") findViewById(R.id.usernameFieldTextView).text = name @@ -65,3 +66,11 @@ class MainActivity : ComponentActivity() { findViewById(R.id.secondNameFieldTextView).text = name } } + +@DistinctUntilChangeFun( + logging = true, + singletonAllow = true +) +private fun printUsernameWithSingletonDistinct(name: String) { + println("printUsernameWithSingletonDistinct: $name") +} diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt index 2a6b059..0d2d1ab 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt @@ -2,18 +2,18 @@ package io.github.stslex.compiler_plugin import io.github.stslex.compiler_plugin.utils.RuntimeLogger -internal object DistinctChangeCache { +internal class DistinctChangeCache( + private val config: DistinctChangeConfig +) { private val cache = mutableMapOf, Any?>>() private val logger = RuntimeLogger.tag("DistinctChangeLogger") - @JvmStatic @Suppress("UNCHECKED_CAST") - fun invoke( + internal operator fun invoke( key: String, args: List, body: () -> R, - config: DistinctChangeConfig ): R { val entry = cache[key] @@ -22,6 +22,9 @@ internal object DistinctChangeCache { } if (entry != null && entry.first == args) { + if (config.logging) { + logger.i("$key not change") + } return entry.second as R } diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt index f19aa8d..04997f6 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt @@ -1,9 +1,14 @@ package io.github.stslex.compiler_plugin +/** + * @param logging enable logs for Kotlin Compiler Runtime work (useful for debug - don't use in production) + * @param singletonAllow if enable - generates distinction for function without classes (so it's singleton) + * */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) public annotation class DistinctUntilChangeFun( - val logging: Boolean = LOGGING_DEFAULT + val logging: Boolean = LOGGING_DEFAULT, + val singletonAllow: Boolean = false ) { public companion object { diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt index b761eb8..1b23139 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt @@ -5,14 +5,18 @@ import io.github.stslex.compiler_plugin.utils.CompileLogger.Companion.toCompiler import io.github.stslex.compiler_plugin.utils.buildLambdaForBody import io.github.stslex.compiler_plugin.utils.buildSaveInCacheCall import io.github.stslex.compiler_plugin.utils.fullyQualifiedName +import io.github.stslex.compiler_plugin.utils.generateFields +import io.github.stslex.compiler_plugin.utils.getQualifierValue import io.github.stslex.compiler_plugin.utils.readQualifier import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.jvm.ir.fileParentOrNull import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.createExpressionBody import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl +import org.jetbrains.kotlin.ir.util.parentClassOrNull internal class IrFunctionTransformer( private val pluginContext: IrPluginContext, @@ -25,6 +29,13 @@ internal class IrFunctionTransformer( val qualifierArgs = pluginContext.readQualifier(declaration, logger) ?: return super.visitSimpleFunction(declaration) + if ( + declaration.getQualifierValue("singletonAllow").not() && + declaration.parentClassOrNull == null + ) { + error("singleton is not allowed for ${declaration.name} in ${declaration.fileParentOrNull}") + } + val originalBody = declaration.body ?: return super.visitSimpleFunction(declaration) logger.i("fullyQualifiedName: ${declaration.fullyQualifiedName}") @@ -37,12 +48,16 @@ internal class IrFunctionTransformer( val argsListExpr = pluginContext.buildArgsListExpression(declaration) val lambdaExpr = pluginContext.buildLambdaForBody(originalBody, declaration) + + val backingField = pluginContext.generateFields(declaration, qualifierArgs, logger) + + logger.i("backingField = $backingField") val memoizeCall = pluginContext.buildSaveInCacheCall( keyLiteral = keyLiteral, argsListExpr = argsListExpr, lambdaExpr = lambdaExpr, function = declaration, - qualifierArgs = qualifierArgs, + backingField = backingField, logger = logger ) @@ -52,4 +67,3 @@ internal class IrFunctionTransformer( } } - diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt index 52263ea..ec5007b 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt @@ -3,25 +3,37 @@ package io.github.stslex.compiler_plugin.utils import io.github.stslex.compiler_plugin.DistinctChangeCache import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.backend.jvm.ir.fileParentOrNull +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrConstructor import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrPackageFragment import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.createExpressionBody import org.jetbrains.kotlin.ir.expressions.IrBody import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.symbols.impl.IrFieldSymbolImpl +import org.jetbrains.kotlin.ir.types.defaultType import org.jetbrains.kotlin.ir.types.typeWith import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.util.defaultType import org.jetbrains.kotlin.ir.util.dump +import org.jetbrains.kotlin.ir.util.file import org.jetbrains.kotlin.ir.util.kotlinFqName +import org.jetbrains.kotlin.ir.util.parentClassOrNull import org.jetbrains.kotlin.ir.util.patchDeclarationParents -import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -88,39 +100,117 @@ internal val IrFunction.fullyQualifiedName: String /** * Create call for [DistinctChangeCache.invoke] */ +@OptIn(UnsafeDuringIrConstructionAPI::class) internal fun IrPluginContext.buildSaveInCacheCall( keyLiteral: IrExpression, argsListExpr: IrExpression, lambdaExpr: IrExpression, function: IrSimpleFunction, - qualifierArgs: IrExpression, - logger: CompileLogger + logger: CompileLogger, + backingField: IrFieldSymbolImpl ): IrExpression { - logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()} with config: ${qualifierArgs.dump()}") + logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()}") + + val distinctChangeClassSymbol = referenceClass(DistinctChangeCache::class.toClassId()) + ?: error("Cannot find DistinctChangeCache") + + val invokeFunSymbol = distinctChangeClassSymbol.owner.declarations + .filterIsInstance() + .firstOrNull { it.name == Name.identifier("invoke") } + ?: error("Cannot find DistinctChangeCache.invoke") - val memoizeFunction = referenceFunctions( - CallableId( - classId = DistinctChangeCache::class.toClassId(), - callableName = Name.identifier("invoke") - ) + val getDistCacheField = IrGetFieldImpl( + startOffset = function.startOffset, + endOffset = function.endOffset, + symbol = backingField, + type = distinctChangeClassSymbol.owner.defaultType, + receiver = function.dispatchReceiverParameter?.let { thisReceiver -> + IrGetValueImpl( + startOffset = function.startOffset, + endOffset = function.endOffset, + symbol = thisReceiver.symbol, + type = thisReceiver.type + ) + }, + origin = null ) - .singleOrNull() - ?: error("Cannot find function DistinctChangeCache.memorize") return IrCallImpl( startOffset = function.startOffset, endOffset = function.endOffset, type = function.returnType, - symbol = memoizeFunction, + symbol = invokeFunSymbol.symbol, typeArgumentsCount = 1, - valueArgumentsCount = 4 + valueArgumentsCount = 3, + origin = null ) - .also { call -> call.patchDeclarationParents(function) } + .also { it.patchDeclarationParents(function.parent) } .apply { + dispatchReceiver = getDistCacheField + putTypeArgument(0, function.returnType) putValueArgument(0, keyLiteral) putValueArgument(1, argsListExpr) putValueArgument(2, lambdaExpr) - putValueArgument(3, qualifierArgs) } +} + +@OptIn(UnsafeDuringIrConstructionAPI::class) +internal fun IrPluginContext.generateFields( + function: IrSimpleFunction, + qualifierArgs: IrExpression, + logger: CompileLogger +): IrFieldSymbolImpl { + logger.i("generateFields for ${function.name} parent: ${function.file}") + + val parentClass = function.parentClassOrNull + val parentFile = function.fileParentOrNull + + val errorNotFound = + "function ${function.name} in ${function.file} couldn't be used with @DistinctUntilChangeFun" + + if (parentClass == null && parentFile == null) error(errorNotFound) + + + val startOffset = parentClass?.startOffset ?: parentFile?.startOffset ?: error(errorNotFound) + val endOffset = parentClass?.endOffset ?: parentFile?.endOffset ?: error(errorNotFound) + + val fieldSymbol = IrFieldSymbolImpl() + + val distinctChangeClass = referenceClass(DistinctChangeCache::class.toClassId()) + ?: error("couldn't find DistinctChangeCache") + + val backingField = irFactory.createField( + startOffset = startOffset, + endOffset = endOffset, + origin = IrDeclarationOrigin.PROPERTY_BACKING_FIELD, + symbol = fieldSymbol, + name = Name.identifier("_distinctCache"), + type = distinctChangeClass.defaultType, + visibility = DescriptorVisibilities.PRIVATE, + isFinal = true, + isExternal = false, + isStatic = parentClass == null, + ) + + val constructorSymbol = distinctChangeClass.owner.declarations + .filterIsInstance() + .firstOrNull { it.isPrimary } + ?: error("Cannot find primary constructor of DistinctChangeCache") + + val callDistInit = IrConstructorCallImpl.fromSymbolOwner( + startOffset = startOffset, + endOffset = endOffset, + type = distinctChangeClass.defaultType, + constructorSymbol = constructorSymbol.symbol + ) + .apply { + putValueArgument(0, qualifierArgs) + } + + backingField.parent = function.parent + backingField.initializer = irFactory.createExpressionBody(callDistInit) + (function.parentClassOrNull ?: function.fileParentOrNull)?.declarations?.add(backingField) + + return fieldSymbol } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt index 90558f7..6a8d186 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt @@ -7,12 +7,16 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.ir.builders.irBoolean import org.jetbrains.kotlin.ir.builders.irCallConstructor import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.IrConst +import org.jetbrains.kotlin.ir.expressions.IrConstKind import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.util.constructors import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.getValueArgument import org.jetbrains.kotlin.ir.util.patchDeclarationParents import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name @OptIn(UnsafeDuringIrConstructionAPI::class) internal fun IrPluginContext.readQualifier( @@ -26,8 +30,8 @@ internal fun IrPluginContext.readQualifier( val irBuilder = createIrBuilder(function) - val currentValue = annotation.getValueArgument(0) - val logging = currentValue ?: irBuilder.irBoolean(LOGGING_DEFAULT) + val logging = annotation.getValueArgument(0) + ?: irBuilder.irBoolean(LOGGING_DEFAULT) val constructorSymbol = referenceClass(DistinctChangeConfig::class.toClassId()) ?.constructors @@ -43,4 +47,28 @@ internal fun IrPluginContext.readQualifier( .apply { putValueArgument(0, logging) } -} \ No newline at end of file +} + +internal fun IrSimpleFunction.getQualifierValue(name: String): Boolean = getAnnotation( + FqName(DistinctUntilChangeFun::class.qualifiedName!!) +) + ?.getValueArgument(Name.identifier(name)) + ?.parseValue() + ?: false + +private inline fun IrExpression.parseValue(): T = when (this) { + is IrConst<*> -> when (kind) { + IrConstKind.Boolean -> value + IrConstKind.Byte -> value + IrConstKind.Char -> value + IrConstKind.Double -> value + IrConstKind.Float -> value + IrConstKind.Int -> value + IrConstKind.Long -> value + IrConstKind.Null -> value + IrConstKind.Short -> value + IrConstKind.String -> value + } + + else -> error("Unsupported type") +} as? T ?: error("${T::class} is not as it expected: $value") \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab40d40..13c03c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ activity = "1.10.0" constraintLayout = "2.2.0" jetbrainsKotlinJvm = "2.0.20" -stslexCompilerPlugin = "0.0.2" +stslexCompilerPlugin = "0.0.3" [libraries] android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }