diff --git a/plugins/atomicfu/atomicfu-compiler/src/org.jetbrains.kotlinx.atomicfu.compiler/extensions/AtomicfuTransformer.kt b/plugins/atomicfu/atomicfu-compiler/src/org.jetbrains.kotlinx.atomicfu.compiler/extensions/AtomicfuTransformer.kt index f527d43e233..1b929a1da09 100644 --- a/plugins/atomicfu/atomicfu-compiler/src/org.jetbrains.kotlinx.atomicfu.compiler/extensions/AtomicfuTransformer.kt +++ b/plugins/atomicfu/atomicfu-compiler/src/org.jetbrains.kotlinx.atomicfu.compiler/extensions/AtomicfuTransformer.kt @@ -29,6 +29,8 @@ private const val TRACE_BASE_TYPE = "TraceBase" private const val GETTER = "atomicfu\$getter" private const val SETTER = "atomicfu\$setter" private const val GET = "get" +private const val GET_VALUE = "getValue" +private const val SET_VALUE = "setValue" private const val ATOMIC_VALUE_FACTORY = "atomic" private const val TRACE = "Trace" private const val INVOKE = "invoke" @@ -101,6 +103,39 @@ class AtomicfuTransformer(private val context: IrPluginContext) { private inner class AtomicTransformer : IrElementTransformer { + override fun visitProperty(declaration: IrProperty, data: IrFunction?): IrStatement { + // Support transformation for delegated properties: + if (declaration.isDelegated && declaration.backingField?.type?.isAtomicValueType() == true) { + declaration.backingField?.let { delegateBackingField -> + delegateBackingField.initializer?.let { + val initializer = it.expression as IrCall + when { + initializer.isAtomicFieldGetter() -> { + // val _a = atomic(0) + // var a: Int by _a + // Accessors of the delegated property `a` are implemented via the generated property `a$delegate`, + // that is the copy of the original `_a`. + // They should be delegated to the value of the original field `_a` instead of `a$delegate`. + + // fun () = a$delegate.value -> _a.value + // fun (value: Int) = { a$delegate.value = value } -> { _a.value = value } + val originalField = initializer.getBackingField() + declaration.transform(DelegatePropertyTransformer(originalField, initializer.dispatchReceiver, false), null) + } + initializer.isAtomicFactory() -> { + // var a by atomic(77) -> var a: Int = 77 + it.expression = initializer.eraseAtomicFactory() + ?: error("Atomic factory was expected but found ${initializer.render()}") + declaration.transform(DelegatePropertyTransformer(delegateBackingField, initializer.dispatchReceiver, true), null) + } + else -> error("Unexpected initializer of the delegated property: $initializer") + } + } + } + } + return super.visitProperty(declaration, data) + } + override fun visitFunction(declaration: IrFunction, data: IrFunction?): IrStatement { return super.visitFunction(declaration, declaration) } @@ -233,6 +268,46 @@ class AtomicfuTransformer(private val context: IrPluginContext) { return super.visitConstructorCall(expression, data) } + private inner class DelegatePropertyTransformer( + val originalField: IrField, + val receiver: IrExpression?, + val isInitializedWithAtomicFactory: Boolean + ): IrElementTransformerVoid() { + override fun visitCall(expression: IrCall): IrExpression { + // Accessors of the delegated property have following signatures: + + // public inline operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) + // public inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T + + // getValue/setValue should get and set the value of the originalField + val name = expression.symbol.owner.name.asString() + if (expression.symbol.isKotlinxAtomicfuPackage() && (name == GET_VALUE || name == SET_VALUE)) { + val type = originalField.type.atomicToValueType() + val isSetter = name == SET_VALUE + val runtimeFunction = getRuntimeFunctionSymbol(name, type) + val dispatchReceiver = if (isInitializedWithAtomicFactory) { + val thisRef = expression.getValueArgument(0)!! + if (thisRef.isConstNull()) null else thisRef + } else { + receiver + } + val fieldAccessors = listOf( + context.buildFieldAccessor(originalField, dispatchReceiver, false), + context.buildFieldAccessor(originalField, dispatchReceiver, true) + ) + return buildCall( + UNDEFINED_OFFSET, UNDEFINED_OFFSET, + target = runtimeFunction, + type = type, + typeArguments = if (runtimeFunction.owner.typeParameters.size == 1) listOf(type) else emptyList(), + valueArguments = if (isSetter) listOf(expression.getValueArgument(2)!!, fieldAccessors[0], fieldAccessors[1]) else + fieldAccessors + ) + } + return super.visitCall(expression) + } + } + private fun IrExpression.getReceiverAccessors(parent: IrFunction?): List? = when { this is IrCall -> getAccessors() @@ -297,27 +372,36 @@ class AtomicfuTransformer(private val context: IrPluginContext) { it.type.isAtomicArrayType() && symbol.owner.name.asString() == GET } ?: false + private fun IrCall.getAccessors(): List = + if (!isArrayElementGetter()) { + val field = getBackingField() + listOf( + context.buildFieldAccessor(field, dispatchReceiver, false), + context.buildFieldAccessor(field, dispatchReceiver, true) + ) + } else { + val index = getValueArgument(0)!! + val arrayGetter = dispatchReceiver as IrCall + val arrayField = arrayGetter.getBackingField() + listOf( + context.buildArrayElementAccessor(arrayField, arrayGetter, index, false), + context.buildArrayElementAccessor(arrayField, arrayGetter, index, true) + ) + } + private fun IrStatement.isTrace() = this is IrCall && (isTraceInvoke() || isTraceAppend()) private fun IrCall.isTraceInvoke(): Boolean = symbol.isKotlinxAtomicfuPackage() && - symbol.owner.name.asString() == INVOKE && - symbol.owner.dispatchReceiverParameter?.type?.isTraceBaseType() == true + symbol.owner.name.asString() == INVOKE && + symbol.owner.dispatchReceiverParameter?.type?.isTraceBaseType() == true private fun IrCall.isTraceAppend(): Boolean = symbol.isKotlinxAtomicfuPackage() && - symbol.owner.name.asString() == APPEND && - symbol.owner.dispatchReceiverParameter?.type?.isTraceBaseType() == true + symbol.owner.name.asString() == APPEND && + symbol.owner.dispatchReceiverParameter?.type?.isTraceBaseType() == true - private fun IrCall.getAccessors(): List { - val isArrayElement = isArrayElementGetter() - val valueType = type.atomicToValueType() - return listOf( - context.buildAccessorLambda(this, valueType, false, isArrayElement), - context.buildAccessorLambda(this, valueType, true, isArrayElement) - ) - } private fun getRuntimeFunctionSymbol(name: String, type: IrType): IrSimpleFunctionSymbol { val functionName = when (name) { @@ -340,11 +424,11 @@ class AtomicfuTransformer(private val context: IrPluginContext) { private fun IrCall.getAtomicFunctionName(): String = symbol.signature?.let { signature -> - if (signature is IdSignature.AccessorSignature) signature.accessorSignature else signature.asPublic() - }?.declarationFqName?.let { name -> - if (name.substringBefore('.') in ATOMIC_VALUE_TYPES) { - name.substringAfter('.') - } else name + signature.getDeclarationNameBySignature()?.let { name -> + if (name.substringBefore('.') in ATOMIC_VALUE_TYPES) { + name.substringAfter('.') + } else name + } } ?: error("Incorrect pattern of the atomic function name: ${symbol.owner.render()}") private fun IrCall.eraseAtomicFactory() = @@ -415,6 +499,9 @@ class AtomicfuTransformer(private val context: IrPluginContext) { symbol.isKotlinxAtomicfuPackage() && symbol.owner.name.asString() == ATOMIC_ARRAY_OF_NULLS_FACTORY && type.isAtomicArrayType() + private fun IrCall.isAtomicFieldGetter(): Boolean = + type.isAtomicValueType() && symbol.owner.name.asString().startsWith(" && this.kind.asString == "Null" -private fun Name.getFieldName() = "".toRegex().find(asString())?.groupValues?.get(1) - ?: error("Getter name ${this.asString()} does not match special name pattern ") +internal fun IrField.getterName() = "" +internal fun IrField.setterName() = "" + +internal fun String.getFieldName() = "".toRegex().find(this)?.groupValues?.get(1) + ?: error("Getter name $this does not match special name pattern ") internal fun IrFunctionAccessExpression.getValueArguments() = (0 until valueArgumentsCount).map { i -> @@ -162,82 +165,108 @@ private fun buildGetField(backingField: IrField, ownerClass: IrExpression?): IrG ) } -internal fun IrPluginContext.buildAccessorLambda( - getter: IrCall, - valueType: IrType, - isSetter: Boolean, - isArrayElement: Boolean -): IrExpression { - val getterCall = if (isArrayElement) getter.dispatchReceiver as IrCall else getter - val type = if (isSetter) buildSetterType(valueType) else buildGetterType(valueType) - val name = if (isSetter) setterName(getterCall) else getterName(getterCall) - val returnType = if (isSetter) irBuiltIns.unitType else valueType - val accessorFunction = irFactory.buildFun { +private fun IrPluginContext.buildDefaultPropertyAccessor(name: String): IrSimpleFunction = + irFactory.buildFun { startOffset = UNDEFINED_OFFSET endOffset = UNDEFINED_OFFSET this.origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR - this.name = Name.identifier(name) this.visibility = DescriptorVisibilities.LOCAL this.isInline = true - this.returnType = returnType - }.apply { - val valueParameter = JsIrBuilder.buildValueParameter(this, name, 0, valueType) + this.name = Name.identifier(name) + } + +internal fun IrPluginContext.buildArrayElementAccessor( + arrayField: IrField, + arrayGetter: IrCall, + index: IrExpression, + isSetter: Boolean +): IrExpression { + val valueType = arrayField.type + val functionType = if (isSetter) buildSetterType(valueType) else buildGetterType(valueType) + val returnType = if (isSetter) irBuiltIns.unitType else valueType + val name = if (isSetter) arrayField.setterName() else arrayField.getterName() + val accessorFunction = buildDefaultPropertyAccessor(name).apply { + val valueParameter = buildValueParameter(this, name, 0, valueType) this.valueParameters = if (isSetter) listOf(valueParameter) else emptyList() - val body = if (isSetter) { - if (isArrayElement) { - val setSymbol = referenceFunction(referenceArrayClass(getterCall.type as IrSimpleType), SET) - val elementIndex = getter.getValueArgument(0)!!.deepCopyWithVariables() - buildCall( - UNDEFINED_OFFSET, UNDEFINED_OFFSET, - target = setSymbol, - type = irBuiltIns.unitType, - origin = IrStatementOrigin.LAMBDA, - valueArguments = listOf(elementIndex, valueParameter.capture()) - ).apply { - dispatchReceiver = getterCall + body = irFactory.buildBlockBody( + listOf( + if (isSetter) { + val setSymbol = referenceFunction(referenceArrayClass(arrayField.type as IrSimpleType), SET) + buildCall( + UNDEFINED_OFFSET, UNDEFINED_OFFSET, + target = setSymbol, + type = irBuiltIns.unitType, + origin = IrStatementOrigin.LAMBDA, + valueArguments = listOf(index, valueParameter.capture()) + ).apply { + this.dispatchReceiver = arrayGetter + } + } else { + val getField = buildGetField(arrayField, arrayGetter.dispatchReceiver) + val getSymbol = referenceFunction(referenceArrayClass(arrayField.type as IrSimpleType), GET) + buildCall( + UNDEFINED_OFFSET, UNDEFINED_OFFSET, + target = getSymbol, + type = valueType, + origin = IrStatementOrigin.LAMBDA, + valueArguments = listOf(index) + ).apply { + dispatchReceiver = getField + } } - } else { - buildSetField(getterCall.getBackingField(), getterCall.dispatchReceiver, valueParameter.capture()) - } - } else { - val getField = buildGetField(getterCall.getBackingField(), getterCall.dispatchReceiver) - if (isArrayElement) { - val getSymbol = referenceFunction(referenceArrayClass(getterCall.type as IrSimpleType), GET) - val elementIndex = getter.getValueArgument(0)!!.deepCopyWithVariables() - buildCall( - UNDEFINED_OFFSET, UNDEFINED_OFFSET, - target = getSymbol, - type = valueType, - origin = IrStatementOrigin.LAMBDA, - valueArguments = listOf(elementIndex) - ).apply { - dispatchReceiver = getField.deepCopyWithVariables() - } - } else { - getField.deepCopyWithVariables() - } - } - this.body = irFactory.buildBlockBody(listOf(body)) - origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR + ) + ) + this.returnType = returnType } return IrFunctionExpressionImpl( UNDEFINED_OFFSET, UNDEFINED_OFFSET, - type, + functionType, accessorFunction, IrStatementOrigin.LAMBDA ) } -private fun IrCall.getBackingField(): IrField { - val correspondingPropertySymbol = symbol.owner.correspondingPropertySymbol!! - return correspondingPropertySymbol.owner.backingField!! +internal fun IrPluginContext.buildFieldAccessor( + field: IrField, + dispatchReceiver: IrExpression?, + isSetter: Boolean +): IrExpression { + val valueType = field.type + val functionType = if (isSetter) buildSetterType(valueType) else buildGetterType(valueType) + val returnType = if (isSetter) irBuiltIns.unitType else valueType + val name = if (isSetter) field.setterName() else field.getterName() + val accessorFunction = buildDefaultPropertyAccessor(name).apply { + val valueParameter = buildValueParameter(this, name, 0, valueType) + valueParameters = if (isSetter) listOf(valueParameter) else emptyList() + body = irFactory.buildBlockBody( + listOf( + if (isSetter) { + buildSetField(field, dispatchReceiver, valueParameter.capture()) + } else { + buildGetField(field, dispatchReceiver) + } + ) + ) + this.returnType = returnType + } + return IrFunctionExpressionImpl( + UNDEFINED_OFFSET, UNDEFINED_OFFSET, + functionType, + accessorFunction, + IrStatementOrigin.LAMBDA + ) } +internal fun IrCall.getBackingField(): IrField = + symbol.owner.correspondingPropertySymbol?.let { propertySymbol -> + propertySymbol.owner.backingField ?: error("Property expected to have backing field") + } ?: error("Atomic property accessor ${this.render()} expected to have non-null correspondingPropertySymbol") + internal fun IrPluginContext.referencePackageFunction( packageName: String, name: String, predicate: (IrFunctionSymbol) -> Boolean = { true } -) = try { +): IrSimpleFunctionSymbol = try { referenceFunctions(FqName("$packageName.$name")).single(predicate) } catch (e: RuntimeException) { error("Exception while looking for the function `$name` in package `$packageName`: ${e.message}") @@ -253,13 +282,32 @@ internal fun IrPluginContext.referenceFunction(classSymbol: IrClassSymbol, funct } private fun IrPluginContext.referenceArrayClass(irType: IrSimpleType): IrClassSymbol { - val afuClassId = (irType.classifier.signature!!.asPublic())!!.declarationFqName - val classId = FqName("$KOTLIN.${AFU_ARRAY_CLASSES[afuClassId]!!}") - return referenceClass(classId)!! + val jsArrayName = irType.getArrayClassFqName() + return referenceClass(jsArrayName) ?: error("Array class $jsArrayName was not found in the context") } -internal fun IrPluginContext.getArrayConstructorSymbol(irType: IrSimpleType, predicate: (IrConstructorSymbol) -> Boolean = { true }): IrConstructorSymbol { - val afuClassId = (irType.classifier.signature!!.asPublic())!!.declarationFqName - val classId = FqName("$KOTLIN.${AFU_ARRAY_CLASSES[afuClassId]!!}") - return referenceConstructors(classId).single(predicate) +internal fun IrPluginContext.getArrayConstructorSymbol( + irType: IrSimpleType, + predicate: (IrConstructorSymbol) -> Boolean = { true } +): IrConstructorSymbol { + val jsArrayName = irType.getArrayClassFqName() + return try { + referenceConstructors(jsArrayName).single(predicate) + } catch (e: RuntimeException) { + error("Array constructor $jsArrayName matching the predicate was not found in the context") + } } + +private fun IrSimpleType.getArrayClassFqName(): FqName = + classifier.signature?.let { signature -> + signature.getDeclarationNameBySignature().let { name -> + AFU_ARRAY_CLASSES[name]?.let { jsArrayName -> + FqName("$KOTLIN.$jsArrayName") + } + } + } ?: error("Unexpected array type ${this.render()}") + +internal fun IdSignature.getDeclarationNameBySignature(): String? { + val commonSignature = if (this is IdSignature.AccessorSignature) accessorSignature else asPublic() + return commonSignature?.declarationFqName +} \ No newline at end of file diff --git a/plugins/atomicfu/atomicfu-compiler/test/org/jetbrains/kotlinx/atomicfu/AtomicfuJsIrTestGenerated.java b/plugins/atomicfu/atomicfu-compiler/test/org/jetbrains/kotlinx/atomicfu/AtomicfuJsIrTestGenerated.java index b813cabd373..7909e5b19ee 100644 --- a/plugins/atomicfu/atomicfu-compiler/test/org/jetbrains/kotlinx/atomicfu/AtomicfuJsIrTestGenerated.java +++ b/plugins/atomicfu/atomicfu-compiler/test/org/jetbrains/kotlinx/atomicfu/AtomicfuJsIrTestGenerated.java @@ -43,6 +43,12 @@ public class AtomicfuJsIrTestGenerated extends AbstractAtomicfuJsIrTest { runTest("plugins/atomicfu/atomicfu-compiler/testData/box/AtomicArrayTest.kt"); } + @Test + @TestMetadata("DelegatedPropertiesTest.kt") + public void testDelegatedPropertiesTest() throws Exception { + runTest("plugins/atomicfu/atomicfu-compiler/testData/box/DelegatedPropertiesTest.kt"); + } + @Test @TestMetadata("ExtensionsTest.kt") public void testExtensionsTest() throws Exception { diff --git a/plugins/atomicfu/atomicfu-compiler/testData/box/DelegatedPropertiesTest.kt b/plugins/atomicfu/atomicfu-compiler/testData/box/DelegatedPropertiesTest.kt new file mode 100644 index 00000000000..7c62be1b0d5 --- /dev/null +++ b/plugins/atomicfu/atomicfu-compiler/testData/box/DelegatedPropertiesTest.kt @@ -0,0 +1,144 @@ +import kotlinx.atomicfu.* +import kotlin.test.* + +private val _topLevelInt = atomic(42) +var topLevelInt: Int by _topLevelInt + +var vIntTopLevel by atomic(55) + +class DelegatedProperties { + private val _a = atomic(42) + var a: Int by _a + + private val _l = atomic(55555555555) + var l: Long by _l + + private val _b = atomic(false) + var b: Boolean by _b + + private val _ref = atomic(A(B(77))) + var ref: A by _ref + + var vInt by atomic(77) + + var vLong by atomic(777777777) + + var vBoolean by atomic(false) + + var vRef by atomic(A(B(77))) + + class A (val b: B) + class B (val n: Int) + + fun testDelegatedAtomicInt() { + assertEquals(42, a) + _a.compareAndSet(42, 56) + assertEquals(56, a) + a = 77 + _a.compareAndSet(77, 66) + assertEquals(66, _a.value) + assertEquals(66, a) + } + + fun testDelegatedAtomicLong() { + assertEquals(55555555555, l) + _l.getAndIncrement() + assertEquals(55555555556, l) + l = 7777777777777 + assertTrue(_l.compareAndSet(7777777777777, 66666666666)) + assertEquals(66666666666, _l.value) + assertEquals(66666666666, l) + } + + fun testDelegatedAtomicBoolean() { + assertEquals(false, b) + _b.lazySet(true) + assertEquals(true, b) + b = false + assertTrue(_b.compareAndSet(false, true)) + assertEquals(true, _b.value) + assertEquals(true, b) + } + + fun testDelegatedAtomicRef() { + assertEquals(77, ref.b.n) + _ref.lazySet(A(B(66))) + assertEquals(66, ref.b.n) + assertTrue(_ref.compareAndSet(_ref.value, A(B(56)))) + assertEquals(56, ref.b.n) + ref = A(B(99)) + assertEquals(99, _ref.value.b.n) + } + + fun testVolatileInt() { + assertEquals(77, vInt) + vInt = 55 + assertEquals(110, vInt * 2) + } + + fun testVolatileLong() { + assertEquals(777777777, vLong) + vLong = 55 + assertEquals(55, vLong) + } + + fun testVolatileBoolean() { + assertEquals(false, vBoolean) + vBoolean = true + assertEquals(true, vBoolean) + } + + fun testVolatileRef() { + assertEquals(77, vRef.b.n) + vRef = A(B(99)) + assertEquals(99, vRef.b.n) + } + + inner class D { + var b: Int by _a + } + + fun testScopedDelegatedProperties() { + val clazz = D() + clazz.b = 42 + _a.compareAndSet(42, 56) + assertEquals(56, clazz.b) + clazz.b = 77 + _a.compareAndSet(77, 66) + assertEquals(66, _a.value) + assertEquals(66, clazz.b) + } + + fun test() { + testDelegatedAtomicInt() + testDelegatedAtomicLong() + testDelegatedAtomicBoolean() + testDelegatedAtomicRef() + testVolatileInt() + testVolatileBoolean() + testVolatileLong() + testVolatileRef() + testScopedDelegatedProperties() + } +} + +fun testTopLevelDelegatedProperties() { + assertEquals(42, topLevelInt) + _topLevelInt.compareAndSet(42, 56) + assertEquals(56, topLevelInt) + topLevelInt = 77 + _topLevelInt.compareAndSet(77, 66) + assertEquals(66, _topLevelInt.value) + assertEquals(66, topLevelInt) + + assertEquals(55, vIntTopLevel) + vIntTopLevel = 70 + assertEquals(140, vIntTopLevel * 2) +} + +fun box(): String { + val testClass = DelegatedProperties() + testClass.test() + //testTopLevelDelegatedProperties() + return "OK" +} \ No newline at end of file