Distinguish numeric comparisons in FE, store info in BindingContext

When we an equality or comparison operator expression
with both arguments "statically known to be of primitive numeric types"
(that is, either inferred type T for an operand is a primitive numeric
type, or a smart cast to a primitive numeric type T is possible in the
corresponding context), comparisons should be performed on primitive
numbers with corresponding widening conversions.

This differs from default 'equals' and 'compareTo' implementations in
case of floating-point numbers: for Float and Double, IEEE 754
comparisons are used instead of total order implemented by j.l.Float and
j.l.Double.

Examples:

fun ex1(x: Double, y: Double) = x < y
-- will use IEEE 754 comparison for Double, because
   both 'x' and 'y' have type Double

fun ex2(x: Double, y: Any) = y is Double && x < y
-- will use IEEE 754 comparison for Double, because
   smart cast to Double is possible for 'y'

fun ex3(x: Comparable<Double>, y: Double) = x is Double && x < y
-- will use IEEE 754 comparison for Double, because
   smart cast to Double is possible for 'x',
   even though corresponding operator convention is resolved to
   'Comparable<Double>#compareTo(Double)' (which would use total order)

fun ex4(x: Any, y: Any) = x is Double && y is Int && x < y
-- will use IEEE 754 comparison for Double with 'y' promoted to Double,
   because smart cast to Double is possible for 'x',
   and smart cast to Int is possible for 'y',
   and least common primitive numeric type for Double and Int is Double.
This commit is contained in:
Dmitry Petrov
2018-02-01 14:11:36 +03:00
parent 930e51e30e
commit f4ed4ec9d9
4 changed files with 116 additions and 26 deletions
@@ -267,6 +267,9 @@ public interface BindingContext {
WritableSlice<KtFunction, KotlinResolutionCallbacksImpl.LambdaInfo> NEW_INFERENCE_LAMBDA_INFO = new BasicWritableSlice<>(DO_NOTHING); WritableSlice<KtFunction, KotlinResolutionCallbacksImpl.LambdaInfo> NEW_INFERENCE_LAMBDA_INFO = new BasicWritableSlice<>(DO_NOTHING);
WritableSlice<KtBinaryExpression, KotlinType> PRIMITIVE_NUMERIC_COMPARISON_TYPE = Slices.createSimpleSlice();
WritableSlice<KtExpression, KotlinType> PRIMITIVE_NUMERIC_COMPARISON_OPERAND_TYPE = Slices.createSimpleSlice();
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
@Deprecated // This field is needed only for the side effects of its initializer @Deprecated // This field is needed only for the side effects of its initializer
Void _static_initializer = BasicWritableSlice.initSliceDebugNames(BindingContext.class); Void _static_initializer = BasicWritableSlice.initSliceDebugNames(BindingContext.class);
@@ -90,7 +90,8 @@ private val DEFAULT_CALL_CHECKERS = listOf(
ConstructorHeaderCallChecker, ProtectedConstructorCallChecker, ApiVersionCallChecker, ConstructorHeaderCallChecker, ProtectedConstructorCallChecker, ApiVersionCallChecker,
CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker, CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker,
CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker, CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker,
UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker() UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(),
PrimitiveNumericComparisonCallChecker
) )
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>() private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf( private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf(
@@ -0,0 +1,75 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.resolve.checkers
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowValueFactory
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.*
object PrimitiveNumericComparisonCallChecker : CallChecker {
private val comparisonOperatorTokens = setOf(KtTokens.EQEQ, KtTokens.EXCLEQ, KtTokens.LT, KtTokens.LTEQ, KtTokens.GT, KtTokens.GTEQ)
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
// Primitive number comparisons only take part in binary operator convention resolution
val binaryExpression = resolvedCall.call.callElement as? KtBinaryExpression ?: return
if (!comparisonOperatorTokens.contains(binaryExpression.operationReference.getReferencedNameElementType())) return
val leftExpr = binaryExpression.left ?: return
val rightExpr = binaryExpression.right ?: return
val leftType = context.getInferredPrimitiveNumericType(leftExpr) ?: return
val rightType = context.getInferredPrimitiveNumericType(rightExpr) ?: return
context.trace.record(BindingContext.PRIMITIVE_NUMERIC_COMPARISON_OPERAND_TYPE, leftExpr, leftType)
context.trace.record(BindingContext.PRIMITIVE_NUMERIC_COMPARISON_OPERAND_TYPE, rightExpr, rightType)
val leastCommonType = leastCommonPrimitiveNumericType(leftType, rightType)
context.trace.record(BindingContext.PRIMITIVE_NUMERIC_COMPARISON_TYPE, binaryExpression, leastCommonType)
}
private fun leastCommonPrimitiveNumericType(t1: KotlinType, t2: KotlinType): KotlinType {
val pt1 = t1.promoteIntegerTypeToIntIfRequired()
val pt2 = t2.promoteIntegerTypeToIntIfRequired()
return when {
pt1.isDouble() || pt2.isDouble() -> t1.builtIns.doubleType
pt1.isFloat() || pt2.isFloat() -> t1.builtIns.floatType
pt1.isLong() || pt2.isLong() -> t1.builtIns.longType
pt1.isInt() || pt2.isInt() -> t1.builtIns.intType
else -> throw AssertionError("Unexpected types: t1=$t1, t2=$t2")
}
}
private fun KotlinType.promoteIntegerTypeToIntIfRequired() =
when {
!isPrimitiveNumberType() -> throw AssertionError("Primitive number type expected: $this")
isByte() || isChar() || isShort() -> builtIns.intType
else -> this
}
private fun CallCheckerContext.getInferredPrimitiveNumericType(expression: KtExpression): KotlinType? {
val type = trace.bindingContext.getType(expression) ?: return null
val dataFlowValue = DataFlowValueFactory.createDataFlowValue(
expression, type, trace.bindingContext, resolutionContext.scope.ownerDescriptor
)
val dataFlowInfo = trace.get(BindingContext.EXPRESSION_TYPE_INFO, expression)?.dataFlowInfo ?: return null
val stableTypes = dataFlowInfo.getStableTypes(dataFlowValue, languageVersionSettings)
return (listOf(type) + stableTypes).findPrimitiveType()
}
private fun List<KotlinType>.findPrimitiveType() =
find { it.isPrimitiveNumberOrNullableType() }?.makeNotNullable()
}
@@ -57,8 +57,20 @@ fun KotlinType.isAnyOrNullableAny(): Boolean = KotlinBuiltIns.isAnyOrNullableAny
fun KotlinType.isNullableAny(): Boolean = KotlinBuiltIns.isNullableAny(this) fun KotlinType.isNullableAny(): Boolean = KotlinBuiltIns.isNullableAny(this)
fun KotlinType.isBoolean(): Boolean = KotlinBuiltIns.isBoolean(this) fun KotlinType.isBoolean(): Boolean = KotlinBuiltIns.isBoolean(this)
fun KotlinType.isPrimitiveNumberType(): Boolean = KotlinBuiltIns.isPrimitiveType(this) && !isBoolean() fun KotlinType.isPrimitiveNumberType(): Boolean = KotlinBuiltIns.isPrimitiveType(this) && !isBoolean()
fun KotlinType.isBooleanOrNullableBoolean(): Boolean = KotlinBuiltIns.isBooleanOrNullableBoolean(this) fun KotlinType.isBooleanOrNullableBoolean(): Boolean = KotlinBuiltIns.isBooleanOrNullableBoolean(this)
fun KotlinType.isNotNullThrowable(): Boolean = KotlinBuiltIns.isThrowableOrNullableThrowable(this) && !isMarkedNullable fun KotlinType.isNotNullThrowable(): Boolean = KotlinBuiltIns.isThrowableOrNullableThrowable(this) && !isMarkedNullable
fun KotlinType.isByte() = KotlinBuiltIns.isByte(this)
fun KotlinType.isChar() = KotlinBuiltIns.isChar(this)
fun KotlinType.isShort() = KotlinBuiltIns.isShort(this)
fun KotlinType.isInt() = KotlinBuiltIns.isInt(this)
fun KotlinType.isLong() = KotlinBuiltIns.isLong(this)
fun KotlinType.isFloat() = KotlinBuiltIns.isFloat(this)
fun KotlinType.isDouble() = KotlinBuiltIns.isDouble(this)
fun KotlinType.isPrimitiveNumberOrNullableType(): Boolean =
KotlinBuiltIns.isPrimitiveTypeOrNullablePrimitiveType(this) &&
!KotlinBuiltIns.isBooleanOrNullableBoolean(this)
fun KotlinType.isTypeParameter(): Boolean = TypeUtils.isTypeParameter(this) fun KotlinType.isTypeParameter(): Boolean = TypeUtils.isTypeParameter(this)
@@ -75,10 +87,10 @@ fun KotlinType?.isArrayOfNothing(): Boolean {
fun KotlinType.isSubtypeOf(superType: KotlinType): Boolean = KotlinTypeChecker.DEFAULT.isSubtypeOf(this, superType) fun KotlinType.isSubtypeOf(superType: KotlinType): Boolean = KotlinTypeChecker.DEFAULT.isSubtypeOf(this, superType)
fun isNullabilityMismatch(expected: KotlinType, actual: KotlinType) = fun isNullabilityMismatch(expected: KotlinType, actual: KotlinType) =
!expected.isMarkedNullable && actual.isMarkedNullable && actual.isSubtypeOf(TypeUtils.makeNullable(expected)) !expected.isMarkedNullable && actual.isMarkedNullable && actual.isSubtypeOf(TypeUtils.makeNullable(expected))
fun KotlinType.cannotBeReified(): Boolean = fun KotlinType.cannotBeReified(): Boolean =
KotlinBuiltIns.isNothingOrNullableNothing(this) || this.isDynamic() || this.isCaptured() KotlinBuiltIns.isNothingOrNullableNothing(this) || this.isDynamic() || this.isCaptured()
fun TypeProjection.substitute(doSubstitute: (KotlinType) -> KotlinType): TypeProjection { fun TypeProjection.substitute(doSubstitute: (KotlinType) -> KotlinType): TypeProjection {
return if (isStarProjection) return if (isStarProjection)
@@ -104,7 +116,7 @@ fun List<KotlinType>.defaultProjections(): List<TypeProjection> = map(::TypeProj
fun KotlinType.isDefaultBound(): Boolean = KotlinBuiltIns.isDefaultBound(getSupertypeRepresentative()) fun KotlinType.isDefaultBound(): Boolean = KotlinBuiltIns.isDefaultBound(getSupertypeRepresentative())
fun createProjection(type: KotlinType, projectionKind: Variance, typeParameterDescriptor: TypeParameterDescriptor?): TypeProjection = fun createProjection(type: KotlinType, projectionKind: Variance, typeParameterDescriptor: TypeParameterDescriptor?): TypeProjection =
TypeProjectionImpl(if (typeParameterDescriptor?.variance == projectionKind) Variance.INVARIANT else projectionKind, type) TypeProjectionImpl(if (typeParameterDescriptor?.variance == projectionKind) Variance.INVARIANT else projectionKind, type)
fun Collection<KotlinType>.closure(f: (KotlinType) -> Collection<KotlinType>): Collection<KotlinType> { fun Collection<KotlinType>.closure(f: (KotlinType) -> Collection<KotlinType>): Collection<KotlinType> {
if (size == 0) return this if (size == 0) return this
@@ -124,7 +136,7 @@ fun Collection<KotlinType>.closure(f: (KotlinType) -> Collection<KotlinType>): C
} }
fun boundClosure(types: Collection<KotlinType>): Collection<KotlinType> = fun boundClosure(types: Collection<KotlinType>): Collection<KotlinType> =
types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() } types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() }
fun constituentTypes(types: Collection<KotlinType>): Collection<KotlinType> { fun constituentTypes(types: Collection<KotlinType>): Collection<KotlinType> {
val result = hashSetOf<KotlinType>() val result = hashSetOf<KotlinType>()
@@ -133,15 +145,14 @@ fun constituentTypes(types: Collection<KotlinType>): Collection<KotlinType> {
} }
fun KotlinType.constituentTypes(): Collection<KotlinType> = fun KotlinType.constituentTypes(): Collection<KotlinType> =
constituentTypes(listOf(this)) constituentTypes(listOf(this))
private fun constituentTypes(result: MutableSet<KotlinType>, types: Collection<KotlinType>) { private fun constituentTypes(result: MutableSet<KotlinType>, types: Collection<KotlinType>) {
result.addAll(types) result.addAll(types)
for (type in types) { for (type in types) {
if (type.isFlexible()) { if (type.isFlexible()) {
with (type.asFlexibleType()) { constituentTypes(result, setOf(lowerBound, upperBound)) } with(type.asFlexibleType()) { constituentTypes(result, setOf(lowerBound, upperBound)) }
} } else {
else {
constituentTypes(result, type.arguments.mapNotNull { if (!it.isStarProjection) it.type else null }) constituentTypes(result, type.arguments.mapNotNull { if (!it.isStarProjection) it.type else null })
} }
} }
@@ -163,8 +174,8 @@ fun KotlinType.replaceArgumentsWithStarProjections(): KotlinType {
val unwrapped = unwrap() val unwrapped = unwrap()
return when (unwrapped) { return when (unwrapped) {
is FlexibleType -> KotlinTypeFactory.flexibleType( is FlexibleType -> KotlinTypeFactory.flexibleType(
unwrapped.lowerBound.replaceArgumentsWithStarProjections(), unwrapped.lowerBound.replaceArgumentsWithStarProjections(),
unwrapped.upperBound.replaceArgumentsWithStarProjections() unwrapped.upperBound.replaceArgumentsWithStarProjections()
) )
is SimpleType -> unwrapped.replaceArgumentsWithStarProjections() is SimpleType -> unwrapped.replaceArgumentsWithStarProjections()
}.inheritEnhancement(unwrapped) }.inheritEnhancement(unwrapped)
@@ -179,24 +190,24 @@ private fun SimpleType.replaceArgumentsWithStarProjections(): SimpleType {
} }
fun KotlinType.containsTypeAliasParameters(): Boolean = fun KotlinType.containsTypeAliasParameters(): Boolean =
contains { contains {
it.constructor.declarationDescriptor?.isTypeAliasParameter() ?: false it.constructor.declarationDescriptor?.isTypeAliasParameter() ?: false
} }
fun KotlinType.containsTypeAliases(): Boolean = fun KotlinType.containsTypeAliases(): Boolean =
contains { contains {
it.constructor.declarationDescriptor is TypeAliasDescriptor it.constructor.declarationDescriptor is TypeAliasDescriptor
} }
fun ClassifierDescriptor.isTypeAliasParameter(): Boolean = fun ClassifierDescriptor.isTypeAliasParameter(): Boolean =
this is TypeParameterDescriptor && containingDeclaration is TypeAliasDescriptor this is TypeParameterDescriptor && containingDeclaration is TypeAliasDescriptor
fun KotlinType.requiresTypeAliasExpansion(): Boolean = fun KotlinType.requiresTypeAliasExpansion(): Boolean =
contains { contains {
it.constructor.declarationDescriptor?.let { it.constructor.declarationDescriptor?.let {
it is TypeAliasDescriptor || it is TypeParameterDescriptor it is TypeAliasDescriptor || it is TypeParameterDescriptor
} ?: false } ?: false
} }
fun KotlinType.containsTypeProjectionsInTopLevelArguments(): Boolean { fun KotlinType.containsTypeProjectionsInTopLevelArguments(): Boolean {
if (isError) return false if (isError) return false
@@ -205,6 +216,6 @@ fun KotlinType.containsTypeProjectionsInTopLevelArguments(): Boolean {
} }
fun UnwrappedType.canHaveUndefinedNullability(): Boolean = fun UnwrappedType.canHaveUndefinedNullability(): Boolean =
constructor is NewTypeVariableConstructor || constructor is NewTypeVariableConstructor ||
constructor.declarationDescriptor is TypeParameterDescriptor || constructor.declarationDescriptor is TypeParameterDescriptor ||
this is NewCapturedType this is NewCapturedType