Skip to content

Commit e667abc

Browse files
committed
- fix singleton issue
- make singleton mode optional for functions without class
1 parent df7c9f9 commit e667abc

File tree

7 files changed

+176
-27
lines changed

7 files changed

+176
-27
lines changed

app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ class MainActivity : ComponentActivity() {
5151
sendToastOfUserChanges(user)
5252
setName(user.name)
5353
setSecondName(user.secondName)
54+
printUsernameWithSingletonDistinct(user.name)
5455
}
5556

56-
@DistinctUntilChangeFun(false)
57+
@DistinctUntilChangeFun(true)
5758
private fun setName(name: String) {
5859
logger.log(Level.INFO, "setName: $name")
5960
findViewById<TextView>(R.id.usernameFieldTextView).text = name
@@ -65,3 +66,11 @@ class MainActivity : ComponentActivity() {
6566
findViewById<TextView>(R.id.secondNameFieldTextView).text = name
6667
}
6768
}
69+
70+
@DistinctUntilChangeFun(
71+
logging = true,
72+
singletonAllow = true
73+
)
74+
private fun printUsernameWithSingletonDistinct(name: String) {
75+
println("printUsernameWithSingletonDistinct: $name")
76+
}

compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ package io.github.stslex.compiler_plugin
22

33
import io.github.stslex.compiler_plugin.utils.RuntimeLogger
44

5-
internal object DistinctChangeCache {
5+
internal class DistinctChangeCache(
6+
private val config: DistinctChangeConfig
7+
) {
68

79
private val cache = mutableMapOf<String, Pair<List<Any?>, Any?>>()
810
private val logger = RuntimeLogger.tag("DistinctChangeLogger")
911

10-
@JvmStatic
1112
@Suppress("UNCHECKED_CAST")
12-
fun <R> invoke(
13+
internal operator fun <R> invoke(
1314
key: String,
1415
args: List<Any?>,
1516
body: () -> R,
16-
config: DistinctChangeConfig
1717
): R {
1818
val entry = cache[key]
1919

@@ -22,6 +22,9 @@ internal object DistinctChangeCache {
2222
}
2323

2424
if (entry != null && entry.first == args) {
25+
if (config.logging) {
26+
logger.i("$key not change")
27+
}
2528
return entry.second as R
2629
}
2730

compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package io.github.stslex.compiler_plugin
22

3+
/**
4+
* @param logging enable logs for Kotlin Compiler Runtime work (useful for debug - don't use in production)
5+
* @param singletonAllow if enable - generates distinction for function without classes (so it's singleton)
6+
* */
37
@Target(AnnotationTarget.FUNCTION)
48
@Retention(AnnotationRetention.BINARY)
59
public annotation class DistinctUntilChangeFun(
6-
val logging: Boolean = LOGGING_DEFAULT
10+
val logging: Boolean = LOGGING_DEFAULT,
11+
val singletonAllow: Boolean = false
712
) {
813

914
public companion object {

compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import io.github.stslex.compiler_plugin.utils.CompileLogger.Companion.toCompiler
55
import io.github.stslex.compiler_plugin.utils.buildLambdaForBody
66
import io.github.stslex.compiler_plugin.utils.buildSaveInCacheCall
77
import io.github.stslex.compiler_plugin.utils.fullyQualifiedName
8+
import io.github.stslex.compiler_plugin.utils.generateFields
9+
import io.github.stslex.compiler_plugin.utils.getQualifierValue
810
import io.github.stslex.compiler_plugin.utils.readQualifier
911
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
1012
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
13+
import org.jetbrains.kotlin.backend.jvm.ir.fileParentOrNull
1114
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
1215
import org.jetbrains.kotlin.ir.IrStatement
1316
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
1417
import org.jetbrains.kotlin.ir.declarations.createExpressionBody
1518
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
19+
import org.jetbrains.kotlin.ir.util.parentClassOrNull
1620

1721
internal class IrFunctionTransformer(
1822
private val pluginContext: IrPluginContext,
@@ -25,6 +29,13 @@ internal class IrFunctionTransformer(
2529
val qualifierArgs = pluginContext.readQualifier(declaration, logger)
2630
?: return super.visitSimpleFunction(declaration)
2731

32+
if (
33+
declaration.getQualifierValue("singletonAllow").not() &&
34+
declaration.parentClassOrNull == null
35+
) {
36+
error("singleton is not allowed for ${declaration.name} in ${declaration.fileParentOrNull}")
37+
}
38+
2839
val originalBody = declaration.body ?: return super.visitSimpleFunction(declaration)
2940

3041
logger.i("fullyQualifiedName: ${declaration.fullyQualifiedName}")
@@ -37,12 +48,16 @@ internal class IrFunctionTransformer(
3748

3849
val argsListExpr = pluginContext.buildArgsListExpression(declaration)
3950
val lambdaExpr = pluginContext.buildLambdaForBody(originalBody, declaration)
51+
52+
val backingField = pluginContext.generateFields(declaration, qualifierArgs, logger)
53+
54+
logger.i("backingField = $backingField")
4055
val memoizeCall = pluginContext.buildSaveInCacheCall(
4156
keyLiteral = keyLiteral,
4257
argsListExpr = argsListExpr,
4358
lambdaExpr = lambdaExpr,
4459
function = declaration,
45-
qualifierArgs = qualifierArgs,
60+
backingField = backingField,
4661
logger = logger
4762
)
4863

@@ -52,4 +67,3 @@ internal class IrFunctionTransformer(
5267
}
5368

5469
}
55-

compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,37 @@ package io.github.stslex.compiler_plugin.utils
33
import io.github.stslex.compiler_plugin.DistinctChangeCache
44
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
55
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
6+
import org.jetbrains.kotlin.backend.jvm.ir.fileParentOrNull
7+
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
68
import org.jetbrains.kotlin.descriptors.Modality
79
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
810
import org.jetbrains.kotlin.ir.declarations.IrClass
11+
import org.jetbrains.kotlin.ir.declarations.IrConstructor
912
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
1013
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
1114
import org.jetbrains.kotlin.ir.declarations.IrFunction
1215
import org.jetbrains.kotlin.ir.declarations.IrPackageFragment
1316
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
17+
import org.jetbrains.kotlin.ir.declarations.createExpressionBody
1418
import org.jetbrains.kotlin.ir.expressions.IrBody
1519
import org.jetbrains.kotlin.ir.expressions.IrExpression
1620
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
1721
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
22+
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
1823
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
24+
import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl
25+
import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
26+
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
27+
import org.jetbrains.kotlin.ir.symbols.impl.IrFieldSymbolImpl
28+
import org.jetbrains.kotlin.ir.types.defaultType
1929
import org.jetbrains.kotlin.ir.types.typeWith
2030
import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols
31+
import org.jetbrains.kotlin.ir.util.defaultType
2132
import org.jetbrains.kotlin.ir.util.dump
33+
import org.jetbrains.kotlin.ir.util.file
2234
import org.jetbrains.kotlin.ir.util.kotlinFqName
35+
import org.jetbrains.kotlin.ir.util.parentClassOrNull
2336
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
24-
import org.jetbrains.kotlin.name.CallableId
2537
import org.jetbrains.kotlin.name.ClassId
2638
import org.jetbrains.kotlin.name.FqName
2739
import org.jetbrains.kotlin.name.Name
@@ -88,39 +100,117 @@ internal val IrFunction.fullyQualifiedName: String
88100
/**
89101
* Create call for [DistinctChangeCache.invoke]
90102
*/
103+
@OptIn(UnsafeDuringIrConstructionAPI::class)
91104
internal fun IrPluginContext.buildSaveInCacheCall(
92105
keyLiteral: IrExpression,
93106
argsListExpr: IrExpression,
94107
lambdaExpr: IrExpression,
95108
function: IrSimpleFunction,
96-
qualifierArgs: IrExpression,
97-
logger: CompileLogger
109+
logger: CompileLogger,
110+
backingField: IrFieldSymbolImpl
98111
): IrExpression {
99-
logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()} with config: ${qualifierArgs.dump()}")
112+
logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()}")
113+
114+
val distinctChangeClassSymbol = referenceClass(DistinctChangeCache::class.toClassId())
115+
?: error("Cannot find DistinctChangeCache")
116+
117+
val invokeFunSymbol = distinctChangeClassSymbol.owner.declarations
118+
.filterIsInstance<IrSimpleFunction>()
119+
.firstOrNull { it.name == Name.identifier("invoke") }
120+
?: error("Cannot find DistinctChangeCache.invoke")
100121

101-
val memoizeFunction = referenceFunctions(
102-
CallableId(
103-
classId = DistinctChangeCache::class.toClassId(),
104-
callableName = Name.identifier("invoke")
105-
)
122+
val getDistCacheField = IrGetFieldImpl(
123+
startOffset = function.startOffset,
124+
endOffset = function.endOffset,
125+
symbol = backingField,
126+
type = distinctChangeClassSymbol.owner.defaultType,
127+
receiver = function.dispatchReceiverParameter?.let { thisReceiver ->
128+
IrGetValueImpl(
129+
startOffset = function.startOffset,
130+
endOffset = function.endOffset,
131+
symbol = thisReceiver.symbol,
132+
type = thisReceiver.type
133+
)
134+
},
135+
origin = null
106136
)
107-
.singleOrNull()
108-
?: error("Cannot find function DistinctChangeCache.memorize")
109137

110138
return IrCallImpl(
111139
startOffset = function.startOffset,
112140
endOffset = function.endOffset,
113141
type = function.returnType,
114-
symbol = memoizeFunction,
142+
symbol = invokeFunSymbol.symbol,
115143
typeArgumentsCount = 1,
116-
valueArgumentsCount = 4
144+
valueArgumentsCount = 3,
145+
origin = null
117146
)
118-
.also { call -> call.patchDeclarationParents(function) }
147+
.also { it.patchDeclarationParents(function.parent) }
119148
.apply {
149+
dispatchReceiver = getDistCacheField
150+
120151
putTypeArgument(0, function.returnType)
121152
putValueArgument(0, keyLiteral)
122153
putValueArgument(1, argsListExpr)
123154
putValueArgument(2, lambdaExpr)
124-
putValueArgument(3, qualifierArgs)
125155
}
156+
}
157+
158+
@OptIn(UnsafeDuringIrConstructionAPI::class)
159+
internal fun IrPluginContext.generateFields(
160+
function: IrSimpleFunction,
161+
qualifierArgs: IrExpression,
162+
logger: CompileLogger
163+
): IrFieldSymbolImpl {
164+
logger.i("generateFields for ${function.name} parent: ${function.file}")
165+
166+
val parentClass = function.parentClassOrNull
167+
val parentFile = function.fileParentOrNull
168+
169+
val errorNotFound =
170+
"function ${function.name} in ${function.file} couldn't be used with @DistinctUntilChangeFun"
171+
172+
if (parentClass == null && parentFile == null) error(errorNotFound)
173+
174+
175+
val startOffset = parentClass?.startOffset ?: parentFile?.startOffset ?: error(errorNotFound)
176+
val endOffset = parentClass?.endOffset ?: parentFile?.endOffset ?: error(errorNotFound)
177+
178+
val fieldSymbol = IrFieldSymbolImpl()
179+
180+
val distinctChangeClass = referenceClass(DistinctChangeCache::class.toClassId())
181+
?: error("couldn't find DistinctChangeCache")
182+
183+
val backingField = irFactory.createField(
184+
startOffset = startOffset,
185+
endOffset = endOffset,
186+
origin = IrDeclarationOrigin.PROPERTY_BACKING_FIELD,
187+
symbol = fieldSymbol,
188+
name = Name.identifier("_distinctCache"),
189+
type = distinctChangeClass.defaultType,
190+
visibility = DescriptorVisibilities.PRIVATE,
191+
isFinal = true,
192+
isExternal = false,
193+
isStatic = parentClass == null,
194+
)
195+
196+
val constructorSymbol = distinctChangeClass.owner.declarations
197+
.filterIsInstance<IrConstructor>()
198+
.firstOrNull { it.isPrimary }
199+
?: error("Cannot find primary constructor of DistinctChangeCache")
200+
201+
val callDistInit = IrConstructorCallImpl.fromSymbolOwner(
202+
startOffset = startOffset,
203+
endOffset = endOffset,
204+
type = distinctChangeClass.defaultType,
205+
constructorSymbol = constructorSymbol.symbol
206+
)
207+
.apply {
208+
putValueArgument(0, qualifierArgs)
209+
}
210+
211+
backingField.parent = function.parent
212+
backingField.initializer = irFactory.createExpressionBody(callDistInit)
213+
(function.parentClassOrNull ?: function.fileParentOrNull)?.declarations?.add(backingField)
214+
215+
return fieldSymbol
126216
}

compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
77
import org.jetbrains.kotlin.ir.builders.irBoolean
88
import org.jetbrains.kotlin.ir.builders.irCallConstructor
99
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
10+
import org.jetbrains.kotlin.ir.expressions.IrConst
11+
import org.jetbrains.kotlin.ir.expressions.IrConstKind
1012
import org.jetbrains.kotlin.ir.expressions.IrExpression
1113
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
1214
import org.jetbrains.kotlin.ir.util.constructors
1315
import org.jetbrains.kotlin.ir.util.getAnnotation
16+
import org.jetbrains.kotlin.ir.util.getValueArgument
1417
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
1518
import org.jetbrains.kotlin.name.FqName
19+
import org.jetbrains.kotlin.name.Name
1620

1721
@OptIn(UnsafeDuringIrConstructionAPI::class)
1822
internal fun IrPluginContext.readQualifier(
@@ -26,8 +30,8 @@ internal fun IrPluginContext.readQualifier(
2630

2731
val irBuilder = createIrBuilder(function)
2832

29-
val currentValue = annotation.getValueArgument(0)
30-
val logging = currentValue ?: irBuilder.irBoolean(LOGGING_DEFAULT)
33+
val logging = annotation.getValueArgument(0)
34+
?: irBuilder.irBoolean(LOGGING_DEFAULT)
3135

3236
val constructorSymbol = referenceClass(DistinctChangeConfig::class.toClassId())
3337
?.constructors
@@ -43,4 +47,28 @@ internal fun IrPluginContext.readQualifier(
4347
.apply {
4448
putValueArgument(0, logging)
4549
}
46-
}
50+
}
51+
52+
internal fun IrSimpleFunction.getQualifierValue(name: String): Boolean = getAnnotation(
53+
FqName(DistinctUntilChangeFun::class.qualifiedName!!)
54+
)
55+
?.getValueArgument(Name.identifier(name))
56+
?.parseValue<Boolean>()
57+
?: false
58+
59+
private inline fun <reified T> IrExpression.parseValue(): T = when (this) {
60+
is IrConst<*> -> when (kind) {
61+
IrConstKind.Boolean -> value
62+
IrConstKind.Byte -> value
63+
IrConstKind.Char -> value
64+
IrConstKind.Double -> value
65+
IrConstKind.Float -> value
66+
IrConstKind.Int -> value
67+
IrConstKind.Long -> value
68+
IrConstKind.Null -> value
69+
IrConstKind.Short -> value
70+
IrConstKind.String -> value
71+
}
72+
73+
else -> error("Unsupported type")
74+
} as? T ?: error("${T::class} is not as it expected: $value")

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ activity = "1.10.0"
1414
constraintLayout = "2.2.0"
1515
jetbrainsKotlinJvm = "2.0.20"
1616

17-
stslexCompilerPlugin = "0.0.2"
17+
stslexCompilerPlugin = "0.0.3"
1818

1919
[libraries]
2020
android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }

0 commit comments

Comments
 (0)