kotlinx.atomicfu compiler plugin (JS): delegated properties supported (#4692)

* kotlinx.atomicfu compiler plugin (JS): delegated properties supported

* NPE handling fixed

* Scoped delegated properties supported
This commit is contained in:
mvicsokolova
2021-12-29 18:16:07 +03:00
committed by GitHub
parent 0884782c60
commit 5c3ce66e99
4 changed files with 370 additions and 85 deletions
@@ -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<IrFunction?> {
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 <get-a>() = a$delegate.value -> _a.value
// fun <set-a>(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<IrExpression>? =
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<IrExpression> =
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<IrExpression> {
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("<get-")
private fun IrConstructorCall.isAtomicArrayConstructor(): Boolean = type.isAtomicArrayType()
private fun IrCall.isReentrantLockFactory(): Boolean =
@@ -5,12 +5,11 @@
package org.jetbrains.kotlinx.atomicfu.compiler.extensions
import org.jetbrains.kotlin.backend.common.deepCopyWithVariables
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder.buildValueParameter
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
@@ -18,6 +17,8 @@ import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.types.impl.*
import org.jetbrains.kotlin.ir.util.IdSignature
import org.jetbrains.kotlin.ir.util.render
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.types.Variance
@@ -120,11 +121,13 @@ internal fun buildGetValue(
internal fun IrPluginContext.buildConstNull() = IrConstImpl.constNull(UNDEFINED_OFFSET, UNDEFINED_OFFSET, irBuiltIns.anyNType)
internal fun getterName(getterCall: IrCall) = "<get-${getterCall.symbol.owner.name.getFieldName()}>"
internal fun setterName(getterCall: IrCall) = "<set-${getterCall.symbol.owner.name.getFieldName()}>"
internal fun IrExpression.isConstNull() = this is IrConst<*> && this.kind.asString == "Null"
private fun Name.getFieldName() = "<get-(\\w+)>".toRegex().find(asString())?.groupValues?.get(1)
?: error("Getter name ${this.asString()} does not match special name pattern <get-fieldName>")
internal fun IrField.getterName() = "<get-${name.asString()}>"
internal fun IrField.setterName() = "<set-${name.asString()}>"
internal fun String.getFieldName() = "<get-(\\w+)>".toRegex().find(this)?.groupValues?.get(1)
?: error("Getter name $this does not match special name pattern <get-fieldName>")
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
}
@@ -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 {
@@ -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"
}