Implement proper contract for generated java.lang.Annotation.hashCode()

#KT-48606 Fixed
This commit is contained in:
Leonid Startsev
2021-09-06 19:29:08 +03:00
committed by Space
parent 94402ba488
commit 6f954462d6
10 changed files with 200 additions and 113 deletions
@@ -455,6 +455,11 @@ class IrBuiltInsOverFir(
it.owner.name == OperatorNameConventions.TIMES && it.owner.valueParameters[0].type == intType
}
override val intXorSymbol: IrSimpleFunctionSymbol
get() = intClass.functions.single {
it.owner.name == OperatorNameConventions.XOR && it.owner.valueParameters[0].type == intType
}
override val extensionToString: IrSimpleFunctionSymbol by lazy {
findFunctions(kotlinPackage, Name.identifier("toString")).first { function ->
function.owner.extensionReceiverParameter?.let { receiver -> receiver.type == anyNType } ?: false
@@ -419,6 +419,12 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/annotations/instances/annotationInstancesEmptyDefault.kt");
}
@Test
@TestMetadata("annotationJvmHashCode.kt")
public void testAnnotationJvmHashCode() throws Exception {
runTest("compiler/testData/codegen/box/annotations/instances/annotationJvmHashCode.kt");
}
@Test
@TestMetadata("annotationToString.kt")
public void testAnnotationToString() throws Exception {
@@ -13,19 +13,19 @@ import org.jetbrains.kotlin.backend.common.ir.copyTo
import org.jetbrains.kotlin.backend.common.ir.createImplicitParameterDeclarationWithWrappedDescriptor
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
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.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrGetValue
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrDelegatingConstructorCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrSetFieldImpl
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.isArray
import org.jetbrains.kotlin.ir.types.isKClass
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
@@ -183,18 +183,13 @@ open class AnnotationImplementationTransformer(val context: BackendContext, val
@Suppress("UNUSED_VARIABLE")
fun implementEqualsAndHashCode(annotationClass: IrClass, implClass: IrClass, originalProps: List<IrProperty>, childProps: List<IrProperty>) {
val creator = MethodsFromAnyGeneratorForLowerings(context, implClass, ANNOTATION_IMPLEMENTATION)
val generator =
creator.LoweringDataClassMemberGenerator(
nameForToString = "@" + annotationClass.fqNameWhenAvailable!!.asString(),
typeForEquals = annotationClass.defaultType
) { type, a, b ->
generatedEquals(this, type, a, b)
}
val generator = AnnotationImplementationMemberGenerator(
context, implClass,
nameForToString = "@" + annotationClass.fqNameWhenAvailable!!.asString(),
) { type, a, b ->
generatedEquals(this, type, a, b)
}
// Manual implementation of equals is required for two reasons:
// 1. `other` should be casted to interface instead of implementation
// 2. Properties should be retrieved using getters without accessing backing fields
// (DataClassMembersGenerator typically tries to access fields)
val eqFun = creator.createEqualsMethodDeclaration()
generator.generateEqualsUsingGetters(eqFun, annotationClass.defaultType, originalProps)
@@ -208,3 +203,49 @@ open class AnnotationImplementationTransformer(val context: BackendContext, val
open fun implementPlatformSpecificParts(annotationClass: IrClass, implClass: IrClass) {}
}
class AnnotationImplementationMemberGenerator(
backendContext: BackendContext,
irClass: IrClass,
val nameForToString: String,
val selectEquals: IrBlockBodyBuilder.(IrType, IrExpression, IrExpression) -> IrExpression,
) : LoweringDataClassMemberGenerator(backendContext, irClass, ANNOTATION_IMPLEMENTATION) {
override fun IrClass.classNameForToString(): String = nameForToString
// From https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/Annotation.html#equals-java.lang.Object-
// ---
// The hash code of an annotation is the sum of the hash codes of its members (including those with default values), as defined below:
// The hash code of an annotation member is (127 times the hash code of the member-name as computed by String.hashCode()) XOR the hash code of the member-value
override fun IrBuilderWithScope.shiftResultOfHashCode(irResultVar: IrVariable): IrExpression = irGet(irResultVar) // no default (* 31)
override fun getHashCodeOf(builder: IrBuilderWithScope, property: IrProperty, irValue: IrExpression): IrExpression = with(builder) {
val propertyValueHashCode = getHashCodeOf(property.backingField!!.type, irValue)
val propertyNameHashCode = getHashCodeOf(backendContext.irBuiltIns.stringType, irString(property.name.toString()))
val multiplied = irCallOp(context.irBuiltIns.intTimesSymbol, context.irBuiltIns.intType, propertyNameHashCode, irInt(127))
return irCallOp(context.irBuiltIns.intXorSymbol, context.irBuiltIns.intType, multiplied, propertyValueHashCode)
}
// Manual implementation of equals is required for two reasons:
// 1. `other` should be casted to interface instead of implementation
// 2. Properties should be retrieved using getters without accessing backing fields
// (DataClassMembersGenerator typically tries to access fields)
fun generateEqualsUsingGetters(equalsFun: IrSimpleFunction, typeForEquals: IrType, properties: List<IrProperty>) = equalsFun.apply {
body = backendContext.createIrBuilder(symbol).irBlockBody {
val irType = typeForEquals
fun irOther() = irGet(valueParameters[0])
fun irThis() = irGet(dispatchReceiverParameter!!)
fun IrProperty.get(receiver: IrExpression) = irCall(getter!!).apply {
dispatchReceiver = receiver
}
+irIfThenReturnFalse(irNotIs(irOther(), irType))
val otherWithCast = irTemporary(irAs(irOther(), irType), "other_with_cast")
for (property in properties) {
val arg1 = property.get(irThis())
val arg2 = property.get(irGet(irType, otherWithCast.symbol))
+irIfThenReturnFalse(irNot(selectEquals(property.getter?.returnType ?: property.backingField!!.type, arg1, arg2)))
}
+irReturnTrue()
}
}
}
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.backend.common.lower
import org.jetbrains.kotlin.backend.common.BackendContext
import org.jetbrains.kotlin.backend.common.lower.MethodsFromAnyGeneratorForLowerings.Companion.isHashCode
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
@@ -38,71 +39,6 @@ class MethodsFromAnyGeneratorForLowerings(val context: BackendContext, val irCla
addValueParameter("other", context.irBuiltIns.anyNType)
}
inner class LoweringDataClassMemberGenerator(
val nameForToString: String,
val typeForEquals: IrType,
val selectEquals: IrBlockBodyBuilder.(IrType, IrExpression, IrExpression) -> IrExpression,
) :
DataClassMembersGenerator(
IrGeneratorContextBase(context.irBuiltIns),
context.ir.symbols.externalSymbolTable,
irClass,
origin
) {
override fun declareSimpleFunction(startOffset: Int, endOffset: Int, functionDescriptor: FunctionDescriptor): IrFunction {
error("Descriptor API shouldn't be used in lowerings")
}
override fun generateSyntheticFunctionParameterDeclarations(irFunction: IrFunction) {
// no-op — irFunction from lowering should already have necessary parameters
}
override fun getProperty(parameter: ValueParameterDescriptor?, irValueParameter: IrValueParameter?): IrProperty? {
error("Descriptor API shouldn't be used in lowerings")
}
override fun transform(typeParameterDescriptor: TypeParameterDescriptor): IrType {
error("Descriptor API shouldn't be used in lowerings")
}
override fun getHashCodeFunctionInfo(type: IrType): HashCodeFunctionInfo {
val symbol = if (type.isArray() || type.isPrimitiveArray()) {
context.irBuiltIns.dataClassArrayMemberHashCodeSymbol
} else {
type.classOrNull?.functions?.singleOrNull { it.owner.isHashCode() } ?:
context.irBuiltIns.anyClass.functions.single { it.owner.name.asString() == "hashCode" }
}
return object : HashCodeFunctionInfo {
override val symbol: IrSimpleFunctionSymbol = symbol
override fun commitSubstituted(irMemberAccessExpression: IrMemberAccessExpression<*>) {}
}
}
override fun IrClass.classNameForToString(): String = nameForToString
fun generateEqualsUsingGetters(equalsFun: IrSimpleFunction, typeForEquals: IrType, properties: List<IrProperty>) = equalsFun.apply {
body = this@MethodsFromAnyGeneratorForLowerings.context.createIrBuilder(symbol).irBlockBody {
val irType = typeForEquals
fun irOther() = irGet(valueParameters[0])
fun irThis() = irGet(dispatchReceiverParameter!!)
fun IrProperty.get(receiver: IrExpression) = irCall(getter!!).apply {
dispatchReceiver = receiver
}
+irIfThenReturnFalse(irNotIs(irOther(), irType))
val otherWithCast = irTemporary(irAs(irOther(), irType), "other_with_cast")
for (property in properties) {
val arg1 = property.get(irThis())
val arg2 = property.get(irGet(irType, otherWithCast.symbol))
+irIfThenReturnFalse(irNot(selectEquals(property.getter?.returnType ?: property.backingField!!.type, arg1, arg2)))
}
+irReturnTrue()
}
}
}
companion object {
fun IrFunction.isToString(): Boolean =
name.asString() == "toString" && extensionReceiverParameter == null && valueParameters.isEmpty()
@@ -118,6 +54,53 @@ class MethodsFromAnyGeneratorForLowerings(val context: BackendContext, val irCla
fun IrClass.collectOverridenSymbols(predicate: (IrFunction) -> Boolean): List<IrSimpleFunctionSymbol> =
superTypes.mapNotNull { it.getClass()?.functions?.singleOrNull(predicate)?.symbol }
}
}
}
open class LoweringDataClassMemberGenerator(
val backendContext: BackendContext,
irClass: IrClass,
origin: IrDeclarationOrigin
) :
DataClassMembersGenerator(
IrGeneratorContextBase(backendContext.irBuiltIns),
backendContext.ir.symbols.externalSymbolTable,
irClass,
origin
) {
override fun declareSimpleFunction(startOffset: Int, endOffset: Int, functionDescriptor: FunctionDescriptor): IrFunction {
error("Descriptor API shouldn't be used in lowerings")
}
override fun generateSyntheticFunctionParameterDeclarations(irFunction: IrFunction) {
// no-op — irFunction from lowering should already have necessary parameters
}
override fun getProperty(parameter: ValueParameterDescriptor?, irValueParameter: IrValueParameter?): IrProperty? {
error("Descriptor API shouldn't be used in lowerings")
}
override fun transform(typeParameterDescriptor: TypeParameterDescriptor): IrType {
error("Descriptor API shouldn't be used in lowerings")
}
override fun getHashCodeFunctionInfo(type: IrType): HashCodeFunctionInfo {
val symbol = if (type.isArray() || type.isPrimitiveArray()) {
context.irBuiltIns.dataClassArrayMemberHashCodeSymbol
} else {
type.classOrNull?.functions?.singleOrNull { it.owner.isHashCode() }
?: context.irBuiltIns.anyClass.functions.single { it.owner.name.asString() == "hashCode" }
}
return object : HashCodeFunctionInfo {
override val symbol: IrSimpleFunctionSymbol = symbol
override fun commitSubstituted(irMemberAccessExpression: IrMemberAccessExpression<*>) {}
}
}
}
@@ -139,6 +139,7 @@ abstract class IrBuiltIns {
abstract val intPlusSymbol: IrSimpleFunctionSymbol
abstract val intTimesSymbol: IrSimpleFunctionSymbol
abstract val intXorSymbol: IrSimpleFunctionSymbol
abstract val extensionToString: IrSimpleFunctionSymbol
abstract val stringPlus: IrSimpleFunctionSymbol
@@ -32,10 +32,7 @@ import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeBuilder
import org.jetbrains.kotlin.ir.types.impl.buildSimpleType
import org.jetbrains.kotlin.ir.types.impl.originalKotlinType
import org.jetbrains.kotlin.ir.util.SymbolTable
import org.jetbrains.kotlin.ir.util.TypeTranslator
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.referenceClassifier
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.scopes.MemberScope
@@ -387,6 +384,11 @@ class IrBuiltInsOverDescriptors(
KotlinTypeChecker.DEFAULT.equalTypes(it.valueParameters[0].type, int)
}.let { symbolTable.referenceSimpleFunction(it) }
override val intXorSymbol: IrSimpleFunctionSymbol =
builtIns.int.unsubstitutedMemberScope.findFirstFunction("xor") {
KotlinTypeChecker.DEFAULT.equalTypes(it.valueParameters[0].type, int)
}.let { symbolTable.referenceSimpleFunction(it) }
override val intPlusSymbol: IrSimpleFunctionSymbol =
builtIns.int.unsubstitutedMemberScope.findFirstFunction("plus") {
KotlinTypeChecker.DEFAULT.equalTypes(it.valueParameters[0].type, int)
@@ -16,7 +16,9 @@ import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
import org.jetbrains.kotlin.ir.expressions.mapTypeParameters
import org.jetbrains.kotlin.ir.expressions.mapValueParameters
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classifierOrNull
@@ -160,7 +162,7 @@ abstract class DataClassMembersGenerator(
+irResultVar
for (property in properties.drop(1)) {
val shiftedResult = irCallOp(context.irBuiltIns.intTimesSymbol, irIntType, irGet(irResultVar), irInt(31))
val shiftedResult = shiftResultOfHashCode(irResultVar)
val irRhs = irCallOp(context.irBuiltIns.intPlusSymbol, irIntType, shiftedResult, getHashCodeOfProperty(property))
+irSet(irResultVar.symbol, irRhs)
}
@@ -175,30 +177,10 @@ abstract class DataClassMembersGenerator(
context.irBuiltIns.intType,
irGetProperty(irThis(), property),
irInt(0),
getHashCodeOf(property, irGetProperty(irThis(), property))
getHashCodeOf(this, property, irGetProperty(irThis(), property))
)
else ->
getHashCodeOf(property, irGetProperty(irThis(), property))
}
}
private fun getHashCodeOf(property: IrProperty, irValue: IrExpression): IrExpression {
val hashCodeFunctionInfo = getHashCodeFunctionInfo(property.backingField!!.type)
val hashCodeFunctionSymbol = hashCodeFunctionInfo.symbol
val hasDispatchReceiver = hashCodeFunctionSymbol.descriptor.dispatchReceiverParameter != null
return irCall(
hashCodeFunctionSymbol,
context.irBuiltIns.intType,
valueArgumentsCount = if (hasDispatchReceiver) 0 else 1,
typeArgumentsCount = 0
).apply {
if (hasDispatchReceiver) {
dispatchReceiver = irValue
} else {
putValueArgument(0, irValue)
}
hashCodeFunctionInfo.commitSubstituted(this)
getHashCodeOf(this, property, irGetProperty(irThis(), property))
}
}
@@ -230,6 +212,32 @@ abstract class DataClassMembersGenerator(
}
}
protected open fun IrBuilderWithScope.shiftResultOfHashCode(irResultVar: IrVariable): IrExpression =
irCallOp(context.irBuiltIns.intTimesSymbol, context.irBuiltIns.intType, irGet(irResultVar), irInt(31))
protected open fun getHashCodeOf(builder: IrBuilderWithScope, property: IrProperty, irValue: IrExpression) =
builder.getHashCodeOf(property.backingField!!.type, irValue)
protected fun IrBuilderWithScope.getHashCodeOf(type: IrType, irValue: IrExpression): IrExpression {
val hashCodeFunctionInfo = getHashCodeFunctionInfo(type)
val hashCodeFunctionSymbol = hashCodeFunctionInfo.symbol
val hasDispatchReceiver = hashCodeFunctionSymbol.descriptor.dispatchReceiverParameter != null
return irCall(
hashCodeFunctionSymbol,
context.irBuiltIns.intType,
valueArgumentsCount = if (hasDispatchReceiver) 0 else 1,
typeArgumentsCount = 0
).apply {
if (hasDispatchReceiver) {
dispatchReceiver = irValue
} else {
putValueArgument(0, irValue)
}
hashCodeFunctionInfo.commitSubstituted(this)
}
}
fun getIrProperty(property: PropertyDescriptor): IrProperty =
irPropertiesByDescriptor[property]
?: throw AssertionError("Class: ${irClass.descriptor}: unexpected property descriptor: $property")
@@ -20,14 +20,14 @@ annotation class Foo(
val bar: Bar
)
data class BarLike(val i:Int, val s: String, val f: Float)
fun makeHC(name: String, value: Any) = (127 * name.hashCode()) xor value.hashCode()
fun box(): String {
val foo1 = Foo(42, "foo", arrayOf("a", "b"), intArrayOf(1,2), Bar::class, Bar(10, "bar", Float.NaN))
val foo2 = Foo(42, "foo", arrayOf("a", "b"), intArrayOf(1,2), Bar::class, Bar(10, "bar", Float.NaN))
if (foo1 != foo2) return "Failed equals ${foo1.toString()} ${foo2.toString()}"
val barlike = BarLike(10, "bar", Float.NaN)
if (barlike.hashCode() != foo1.bar.hashCode()) return "Failed HC1"
if (barlike.hashCode() != foo2.bar.hashCode()) return "Failed HC2"
val barlike = makeHC("i", 10) + makeHC("s", "bar") + makeHC("f", Float.NaN)
if (barlike != foo1.bar.hashCode()) return "Failed HC1"
if (barlike != foo2.bar.hashCode()) return "Failed HC2"
return "OK"
}
@@ -0,0 +1,35 @@
// IGNORE_BACKEND_FIR: JVM_IR
// TARGET_BACKEND: JVM_IR
// WITH_RUNTIME
// !LANGUAGE: +InstantiationOfAnnotationClasses
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertTrue as assert
annotation class ZeroArg()
annotation class OneArg(val arg: String)
annotation class ManyArg(val i: Int, val o: OneArg, val z: Boolean, val k: KClass<*>, val e: IntArray)
@ZeroArg
@OneArg("a")
@ManyArg(42, OneArg("b"), true, OneArg::class, intArrayOf(1, 2, 3))
class Target
fun box(): String {
val reflectiveZero = Target::class.java.getAnnotation(ZeroArg::class.java)
val reflectiveOne = Target::class.java.getAnnotation(OneArg::class.java)
val reflectiveMany = Target::class.java.getAnnotation(ManyArg::class.java)
val createdZero = ZeroArg()
val createdOne = OneArg("a")
val createdMany = ManyArg(42, OneArg("b"), true, OneArg::class, intArrayOf(1, 2, 3))
assertEquals(reflectiveZero.hashCode(), createdZero.hashCode(), "zero")
assertEquals(reflectiveOne.hashCode(), createdOne.hashCode(), "one")
assertEquals(reflectiveMany.hashCode(), createdMany.hashCode(), "many")
return "OK"
}
@@ -419,6 +419,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/annotations/instances/annotationInstancesEmptyDefault.kt");
}
@Test
@TestMetadata("annotationJvmHashCode.kt")
public void testAnnotationJvmHashCode() throws Exception {
runTest("compiler/testData/codegen/box/annotations/instances/annotationJvmHashCode.kt");
}
@Test
@TestMetadata("annotationToString.kt")
public void testAnnotationToString() throws Exception {