FIR DFA: remove exactNotType

Literally not used for anything anymore. What a waste of CPU time.

...and safeCallBreakInsideDoWhile is broken again. Oh well.
This commit is contained in:
pyos
2022-11-08 21:30:00 +01:00
committed by teamcity
parent 6ee6d019ee
commit 757921e63e
11 changed files with 77 additions and 118 deletions
@@ -388,10 +388,11 @@ digraph jumpFromRhsOfOperator_kt {
153 [label="Exit when"]; 153 [label="Exit when"];
} }
154 [label="Access variable R|<local>/a|"]; 154 [label="Access variable R|<local>/a|"];
155 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"]; 155 [label="Smart cast: R|<local>/a|"];
156 [label="Exit block"]; 156 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"];
157 [label="Exit block"];
} }
157 [label="Exit function test_7" style="filled" fillcolor=red]; 158 [label="Exit function test_7" style="filled" fillcolor=red];
} }
130 -> {131}; 130 -> {131};
131 -> {132}; 131 -> {132};
@@ -404,7 +405,7 @@ digraph jumpFromRhsOfOperator_kt {
138 -> {143 139}; 138 -> {143 139};
139 -> {140}; 139 -> {140};
140 -> {141}; 140 -> {141};
141 -> {157} [label=onUncaughtException]; 141 -> {158} [label=onUncaughtException];
141 -> {142} [style=dotted]; 141 -> {142} [style=dotted];
142 -> {143} [style=dotted]; 142 -> {143} [style=dotted];
143 -> {144}; 143 -> {144};
@@ -421,54 +422,55 @@ digraph jumpFromRhsOfOperator_kt {
154 -> {155}; 154 -> {155};
155 -> {156}; 155 -> {156};
156 -> {157}; 156 -> {157};
157 -> {158};
subgraph cluster_32 { subgraph cluster_32 {
color=red color=red
158 [label="Enter function test_8" style="filled" fillcolor=red]; 159 [label="Enter function test_8" style="filled" fillcolor=red];
subgraph cluster_33 { subgraph cluster_33 {
color=blue color=blue
159 [label="Enter block"]; 160 [label="Enter block"];
subgraph cluster_34 { subgraph cluster_34 {
color=blue color=blue
160 [label="Enter when"]; 161 [label="Enter when"];
subgraph cluster_35 { subgraph cluster_35 {
color=blue color=blue
161 [label="Enter when branch condition "]; 162 [label="Enter when branch condition "];
subgraph cluster_36 { subgraph cluster_36 {
color=blue color=blue
162 [label="Enter &&"]; 163 [label="Enter &&"];
163 [label="Access variable R|<local>/a|"]; 164 [label="Access variable R|<local>/a|"];
164 [label="Const: Null(null)"]; 165 [label="Const: Null(null)"];
165 [label="Equality operator !="]; 166 [label="Equality operator !="];
166 [label="Exit left part of &&"]; 167 [label="Exit left part of &&"];
167 [label="Enter right part of &&"]; 168 [label="Enter right part of &&"];
168 [label="Function call: R|java/lang/Exception.Exception|()"]; 169 [label="Function call: R|java/lang/Exception.Exception|()"];
169 [label="Throw: throw R|java/lang/Exception.Exception|()"]; 170 [label="Throw: throw R|java/lang/Exception.Exception|()"];
170 [label="Stub" style="filled" fillcolor=gray]; 171 [label="Stub" style="filled" fillcolor=gray];
171 [label="Exit &&"]; 172 [label="Exit &&"];
} }
172 [label="Exit when branch condition"]; 173 [label="Exit when branch condition"];
} }
173 [label="Synthetic else branch"]; 174 [label="Synthetic else branch"];
174 [label="Enter when branch result"]; 175 [label="Enter when branch result"];
subgraph cluster_37 { subgraph cluster_37 {
color=blue color=blue
175 [label="Enter block"]; 176 [label="Enter block"];
176 [label="Access variable R|<local>/a|"]; 177 [label="Access variable R|<local>/a|"];
177 [label="Smart cast: R|<local>/a|"]; 178 [label="Smart cast: R|<local>/a|"];
178 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"]; 179 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"];
179 [label="Exit block"]; 180 [label="Exit block"];
} }
180 [label="Exit when branch result"]; 181 [label="Exit when branch result"];
181 [label="Exit when"]; 182 [label="Exit when"];
} }
182 [label="Access variable R|<local>/a|"]; 183 [label="Access variable R|<local>/a|"];
183 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"]; 184 [label="Smart cast: R|<local>/a|"];
184 [label="Exit block"]; 185 [label="Function call: R|<local>/a|.<Inapplicable(UNSAFE_CALL): /A.foo>#()"];
186 [label="Exit block"];
} }
185 [label="Exit function test_8" style="filled" fillcolor=red]; 187 [label="Exit function test_8" style="filled" fillcolor=red];
} }
158 -> {159};
159 -> {160}; 159 -> {160};
160 -> {161}; 160 -> {161};
161 -> {162}; 161 -> {162};
@@ -476,16 +478,16 @@ digraph jumpFromRhsOfOperator_kt {
163 -> {164}; 163 -> {164};
164 -> {165}; 164 -> {165};
165 -> {166}; 165 -> {166};
166 -> {171 167}; 166 -> {167};
167 -> {168}; 167 -> {172 168};
168 -> {169}; 168 -> {169};
169 -> {185} [label=onUncaughtException]; 169 -> {170};
169 -> {170} [style=dotted]; 170 -> {187} [label=onUncaughtException];
170 -> {171} [style=dotted]; 170 -> {171} [style=dotted];
171 -> {172}; 171 -> {172} [style=dotted];
172 -> {174 173}; 172 -> {173};
173 -> {181}; 173 -> {175 174};
174 -> {175}; 174 -> {182};
175 -> {176}; 175 -> {176};
176 -> {177}; 176 -> {177};
177 -> {178}; 177 -> {178};
@@ -496,5 +498,7 @@ digraph jumpFromRhsOfOperator_kt {
182 -> {183}; 182 -> {183};
183 -> {184}; 183 -> {184};
184 -> {185}; 184 -> {185};
185 -> {186};
186 -> {187};
} }
@@ -181,9 +181,7 @@ object FirReturnsImpliesAnalyzer : FirControlFlowChecker() {
} }
} }
is ConeIsInstancePredicate -> is ConeIsInstancePredicate ->
getOrCreateRealVariable(arg)?.let { if (isNegated == inverted) getOrCreateRealVariable(arg)?.let { it typeEq type }?.singleton() else mapOf()
if (isNegated == inverted) it typeEq type else it typeNotEq type
}?.singleton()
is ConeIsNullPredicate -> is ConeIsNullPredicate ->
getOrCreateRealVariable(arg)?.nullabilityStatement(context.session.builtinTypes, isNull = isNegated == inverted) getOrCreateRealVariable(arg)?.nullabilityStatement(context.session.builtinTypes, isNull = isNegated == inverted)
?.singleton() ?.singleton()
@@ -195,10 +193,7 @@ object FirReturnsImpliesAnalyzer : FirControlFlowChecker() {
} }
private fun RealVariable.nullabilityStatement(builtinTypes: BuiltinTypes, isNull: Boolean) = private fun RealVariable.nullabilityStatement(builtinTypes: BuiltinTypes, isNull: Boolean) =
if (isNull) this typeEq (if (isNull) builtinTypes.nullableNothingType.type else builtinTypes.anyType.type)
this typeEq builtinTypes.nullableNothingType.type andTypeNotEq builtinTypes.anyType.type
else
this typeEq builtinTypes.anyType.type andTypeNotEq builtinTypes.nullableNothingType.type
private fun TypeStatement.singleton(): TypeStatements = private fun TypeStatement.singleton(): TypeStatements =
mapOf(variable to this) mapOf(variable to this)
@@ -433,7 +433,6 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
val expressionVariable = variableStorage.createSyntheticVariable(typeOperatorCall) val expressionVariable = variableStorage.createSyntheticVariable(typeOperatorCall)
if (operandVariable.isReal()) { if (operandVariable.isReal()) {
flow.addImplication((expressionVariable eq isType) implies (operandVariable typeEq type)) flow.addImplication((expressionVariable eq isType) implies (operandVariable typeEq type))
flow.addImplication((expressionVariable notEq isType) implies (operandVariable typeNotEq type))
} }
if (!type.canBeNull) { if (!type.canBeNull) {
flow.addImplication((expressionVariable eq isType) implies (operandVariable notEq null)) flow.addImplication((expressionVariable eq isType) implies (operandVariable notEq null))
@@ -459,9 +458,6 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
flow.addImplication((expressionVariable notEq null) implies (operandVariable notEq null)) flow.addImplication((expressionVariable notEq null) implies (operandVariable notEq null))
if (operandVariable.isReal()) { if (operandVariable.isReal()) {
flow.addImplication((expressionVariable notEq null) implies (operandVariable typeEq type)) flow.addImplication((expressionVariable notEq null) implies (operandVariable typeEq type))
if (!type.canBeNull) {
flow.addImplication((expressionVariable eq null) implies (operandVariable typeNotEq type))
}
} }
} }
@@ -614,12 +610,10 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
if (leftOperandVariable.isReal()) { if (leftOperandVariable.isReal()) {
flow.addImplication((expressionVariable eq isEq) implies (leftOperandVariable typeEq rightOperandType)) flow.addImplication((expressionVariable eq isEq) implies (leftOperandVariable typeEq rightOperandType))
flow.addImplication((expressionVariable notEq isEq) implies (leftOperandVariable typeNotEq rightOperandType))
} }
if (rightOperandVariable.isReal()) { if (rightOperandVariable.isReal()) {
flow.addImplication((expressionVariable eq isEq) implies (rightOperandVariable typeEq leftOperandType)) flow.addImplication((expressionVariable eq isEq) implies (rightOperandVariable typeEq leftOperandType))
flow.addImplication((expressionVariable notEq isEq) implies (rightOperandVariable typeNotEq leftOperandType))
} }
} }
@@ -686,7 +680,7 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
private fun FLOW.assumeNotNull(variable: DataFlowVariable, shouldForkFlow: Boolean, shouldRemoveSynthetics: Boolean): FLOW = private fun FLOW.assumeNotNull(variable: DataFlowVariable, shouldForkFlow: Boolean, shouldRemoveSynthetics: Boolean): FLOW =
logicSystem.approveStatementsInsideFlow(this, variable notEq null, shouldForkFlow, shouldRemoveSynthetics).also { logicSystem.approveStatementsInsideFlow(this, variable notEq null, shouldForkFlow, shouldRemoveSynthetics).also {
if (variable is RealVariable) { if (variable is RealVariable) {
it.addTypeStatement(variable typeEq any andTypeNotEq nullableNothing) it.addTypeStatement(variable typeEq any)
} }
} }
@@ -1446,8 +1440,8 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
val variable = effect.variable val variable = effect.variable
if (variable.isReal()) { if (variable.isReal()) {
when (effect.operation) { when (effect.operation) {
Operation.EqNull -> variable typeEq nullableNothing andTypeNotEq any Operation.EqNull -> variable typeEq nullableNothing
Operation.NotEqNull -> variable typeEq any andTypeNotEq nullableNothing Operation.NotEqNull -> variable typeEq any
else -> null else -> null
}?.let { logicSystem.addImplication(this, statement.condition implies it) } }?.let { logicSystem.addImplication(this, statement.condition implies it) }
} }
@@ -91,31 +91,18 @@ abstract class LogicSystem<FLOW : Flow>(protected val context: ConeInferenceCont
} }
} }
// ------------------------------- Util functions ------------------------------- private inline fun Collection<TypeStatement>.singleOrNew(exactType: () -> MutableSet<ConeKotlinType>): TypeStatement =
when (size) {
private fun foldStatements(statements: Collection<TypeStatement>, all: Boolean): TypeStatement { 0 -> throw AssertionError("need at least one statement")
require(statements.isNotEmpty()) 1 -> first()
statements.singleOrNull()?.let { return it } else -> {
val variable = statements.first().variable val variable = first().variable
assert(statements.all { it.variable == variable }) assert(all { it.variable == variable }) { "folding statements for different variables" }
// TypeStatement(variable, exactType, exactNotType) = MutableTypeStatement(variable, exactType())
// variable is intersect(exactType) && variable !is intersect(exactNotType) }
// So `and` of two type statements computes `and` of exactType and `or` of `exactNotType`,
// while `or` is the opposite.
return if (all) {
val exactType = statements.flatMapTo(mutableSetOf()) { it.exactType }
// variable !is a && variable !is b =/=> variable !is commonSuperType(a, b)
// So in this case we can only take the union if either type is a subtype of the other.
val exactNotType = unifyTypes(statements.map { it.exactNotType }, onlyInputTypes = true)
MutableTypeStatement(variable, exactType, exactNotType?.let { mutableSetOf(it) } ?: mutableSetOf())
} else {
val exactType = unifyTypes(statements.map { it.exactType }, onlyInputTypes = false)
val exactNotType = statements.flatMapTo(mutableSetOf()) { it.exactNotType }
MutableTypeStatement(variable, exactType?.let { mutableSetOf(it) } ?: mutableSetOf(), exactNotType)
} }
}
private fun unifyTypes(types: Collection<Set<ConeKotlinType>>, onlyInputTypes: Boolean): ConeKotlinType? { private fun unifyTypes(types: Collection<Set<ConeKotlinType>>): ConeKotlinType? {
if (types.any { it.isEmpty() }) return null if (types.any { it.isEmpty() }) return null
val intersected = types.map { ConeTypeIntersector.intersectTypes(context, it.toList()) } val intersected = types.map { ConeTypeIntersector.intersectTypes(context, it.toList()) }
val unified = context.commonSuperTypeOrNull(intersected) ?: return null val unified = context.commonSuperTypeOrNull(intersected) ?: return null
@@ -123,14 +110,14 @@ abstract class LogicSystem<FLOW : Flow>(protected val context: ConeInferenceCont
unified.isAcceptableForSmartcast() -> unified unified.isAcceptableForSmartcast() -> unified
unified.canBeNull -> null unified.canBeNull -> null
else -> context.anyType() else -> context.anyType()
}.takeIf { !onlyInputTypes || it in intersected } }
} }
protected fun and(statements: Collection<TypeStatement>): TypeStatement = protected fun and(statements: Collection<TypeStatement>): TypeStatement =
foldStatements(statements, all = true) statements.singleOrNew { statements.flatMapTo(mutableSetOf()) { it.exactType } }
protected fun or(statements: Collection<TypeStatement>): TypeStatement = protected fun or(statements: Collection<TypeStatement>): TypeStatement =
foldStatements(statements, all = false) statements.singleOrNew { unifyTypes(statements.map { it.exactType })?.let { mutableSetOf(it) } ?: mutableSetOf() }
} }
/* /*
@@ -16,7 +16,6 @@ import kotlin.math.max
data class PersistentTypeStatement( data class PersistentTypeStatement(
override val variable: RealVariable, override val variable: RealVariable,
override val exactType: PersistentSet<ConeKotlinType>, override val exactType: PersistentSet<ConeKotlinType>,
override val exactNotType: PersistentSet<ConeKotlinType>
) : TypeStatement() ) : TypeStatement()
typealias PersistentApprovedTypeStatements = PersistentMap<RealVariable, PersistentTypeStatement> typealias PersistentApprovedTypeStatements = PersistentMap<RealVariable, PersistentTypeStatement>
@@ -391,7 +390,7 @@ private fun lowestCommonFlow(left: PersistentFlow, right: PersistentFlow): Persi
private fun TypeStatement.toPersistent(): PersistentTypeStatement = when (this) { private fun TypeStatement.toPersistent(): PersistentTypeStatement = when (this) {
is PersistentTypeStatement -> this is PersistentTypeStatement -> this
else -> PersistentTypeStatement(variable, exactType.toPersistentSet(), exactNotType.toPersistentSet()) else -> PersistentTypeStatement(variable, exactType.toPersistentSet())
} }
@JvmName("replaceVariableInStatements") @JvmName("replaceVariableInStatements")
@@ -436,5 +435,5 @@ private fun Statement.replaceVariable(from: RealVariable, to: RealVariable): Sta
if (variable != from) this else when (this) { if (variable != from) this else when (this) {
is OperationStatement -> copy(variable = to) is OperationStatement -> copy(variable = to)
is PersistentTypeStatement -> copy(variable = to) is PersistentTypeStatement -> copy(variable = to)
is MutableTypeStatement -> MutableTypeStatement(to, exactType, exactNotType) is MutableTypeStatement -> MutableTypeStatement(to, exactType)
} }
@@ -15,9 +15,8 @@ import kotlin.contracts.contract
class MutableTypeStatement( class MutableTypeStatement(
override val variable: RealVariable, override val variable: RealVariable,
override val exactType: MutableSet<ConeKotlinType> = linkedSetOf(), override val exactType: MutableSet<ConeKotlinType> = linkedSetOf(),
override val exactNotType: MutableSet<ConeKotlinType> = linkedSetOf()
) : TypeStatement() { ) : TypeStatement() {
fun copy(): MutableTypeStatement = MutableTypeStatement(variable, LinkedHashSet(exactType), LinkedHashSet(exactNotType)) fun copy(): MutableTypeStatement = MutableTypeStatement(variable, LinkedHashSet(exactType))
} }
fun Implication.invertCondition(): Implication = Implication(condition.invert(), effect) fun Implication.invertCondition(): Implication = Implication(condition.invert(), effect)
@@ -49,25 +48,7 @@ infix fun DataFlowVariable.notEq(constant: Boolean?): OperationStatement {
infix fun OperationStatement.implies(effect: Statement): Implication = Implication(this, effect) infix fun OperationStatement.implies(effect: Statement): Implication = Implication(this, effect)
infix fun RealVariable.typeEq(type: ConeKotlinType): MutableTypeStatement = infix fun RealVariable.typeEq(type: ConeKotlinType): MutableTypeStatement =
MutableTypeStatement(this) andTypeEq type MutableTypeStatement(this, if (type is ConeErrorType) linkedSetOf() else linkedSetOf(type))
infix fun RealVariable.typeNotEq(type: ConeKotlinType): MutableTypeStatement =
MutableTypeStatement(this) andTypeNotEq type
infix fun MutableTypeStatement.andTypeEq(type: ConeKotlinType): MutableTypeStatement =
this.apply {
if (type !is ConeErrorType) {
exactType += type
}
}
infix fun MutableTypeStatement.andTypeNotEq(type: ConeKotlinType): MutableTypeStatement =
this.apply {
require(exactNotType.isEmpty()) { "statement $this already has a negation; use `logicSystem.and`" }
if (type !is ConeErrorType) {
exactNotType += type
}
}
// --------------------------------------- Utils --------------------------------------- // --------------------------------------- Utils ---------------------------------------
@@ -31,16 +31,15 @@ data class OperationStatement(override val variable: DataFlowVariable, val opera
sealed class TypeStatement : Statement() { sealed class TypeStatement : Statement() {
abstract override val variable: RealVariable abstract override val variable: RealVariable
abstract val exactType: Set<ConeKotlinType> abstract val exactType: Set<ConeKotlinType>
abstract val exactNotType: Set<ConeKotlinType>
val isEmpty: Boolean val isEmpty: Boolean
get() = exactType.isEmpty() && exactNotType.isEmpty() get() = exactType.isEmpty()
val isNotEmpty: Boolean val isNotEmpty: Boolean
get() = !isEmpty get() = !isEmpty
override fun toString(): String { override fun toString(): String {
return "$variable: $exactType, $exactNotType" return "$variable: $exactType"
} }
} }
@@ -8,7 +8,7 @@ fun calc(x: String?, y: String?): Int {
// x is not null in condition but we do not see it yet // x is not null in condition but we do not see it yet
} while (x.length > 0) } while (x.length > 0)
// y is nullable because of break // y is nullable because of break
y<!UNSAFE_CALL!>.<!>length y.length
// x is not null, at least in theory // x is not null, at least in theory
return x.length return x.length
} }
@@ -102,7 +102,7 @@ fun case_11(x: TypealiasString, y: TypealiasString) {
if (<!SENSELESS_COMPARISON!>y == null<!>) { if (<!SENSELESS_COMPARISON!>y == null<!>) {
if (<!SENSELESS_COMPARISON!>stringProperty != null<!>) { if (<!SENSELESS_COMPARISON!>stringProperty != null<!>) {
if (false || false || false || <!SENSELESS_COMPARISON!>z == null<!> || false) { if (false || false || false || <!SENSELESS_COMPARISON!>z == null<!> || false) {
<!DEBUG_INFO_EXPRESSION_TYPE("TypealiasString")!>x<!> <!DEBUG_INFO_EXPRESSION_TYPE("TypealiasString & kotlin.Nothing")!>x<!>
} }
} }
} }
@@ -134,8 +134,8 @@ fun case_14(x: Any?) {
if (x == null) { if (x == null) {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!> <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>?.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>?.equals(10)
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>x!!<!>.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Nothing")!>x!!<!>.equals(10)
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Any")!>x<!>.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing")!>x<!>.equals(10)
} }
} }
@@ -148,8 +148,8 @@ fun case_15(x: Any?) {
if (x !== null) else { if (x !== null) else {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!> <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>?.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing?")!>x<!>?.equals(10)
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>x!!<!>.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Nothing")!>x!!<!>.equals(10)
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Any")!>x<!>.equals(10) <!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any? & kotlin.Nothing")!>x<!>.equals(10)
} }
} }
@@ -840,8 +840,8 @@ fun case_46(x: TypealiasNullableString<!REDUNDANT_NULLABLE!>?<!>, y: TypealiasNu
if (y === nullableNothingProperty) { if (y === nullableNothingProperty) {
if (z != nullableStringProperty) { if (z != nullableStringProperty) {
if (z === t || <!SENSELESS_COMPARISON!>t == nullableNothingProperty<!>) { if (z === t || <!SENSELESS_COMPARISON!>t == nullableNothingProperty<!>) {
<!DEBUG_INFO_EXPRESSION_TYPE("TypealiasNullableString?")!>x<!> <!DEBUG_INFO_EXPRESSION_TYPE("TypealiasNullableString? & kotlin.Nothing?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("TypealiasNullableString?")!>x<!>.hashCode() <!DEBUG_INFO_EXPRESSION_TYPE("TypealiasNullableString? & kotlin.Nothing?")!>x<!>.hashCode()
} }
} }
} }