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<KtBinaryExpression, KotlinType> PRIMITIVE_NUMERIC_COMPARISON_TYPE = Slices.createSimpleSlice();
WritableSlice<KtExpression, KotlinType> PRIMITIVE_NUMERIC_COMPARISON_OPERAND_TYPE = Slices.createSimpleSlice();
@SuppressWarnings("UnusedDeclaration")
@Deprecated // This field is needed only for the side effects of its initializer
Void _static_initializer = BasicWritableSlice.initSliceDebugNames(BindingContext.class);
@@ -90,7 +90,8 @@ private val DEFAULT_CALL_CHECKERS = listOf(
ConstructorHeaderCallChecker, ProtectedConstructorCallChecker, ApiVersionCallChecker,
CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker,
CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker,
UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker()
UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(),
PrimitiveNumericComparisonCallChecker
)
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
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.isBoolean(): Boolean = KotlinBuiltIns.isBoolean(this)
fun KotlinType.isPrimitiveNumberType(): Boolean = KotlinBuiltIns.isPrimitiveType(this) && !isBoolean()
fun KotlinType.isBooleanOrNullableBoolean(): Boolean = KotlinBuiltIns.isBooleanOrNullableBoolean(this)
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)
@@ -75,10 +87,10 @@ fun KotlinType?.isArrayOfNothing(): Boolean {
fun KotlinType.isSubtypeOf(superType: KotlinType): Boolean = KotlinTypeChecker.DEFAULT.isSubtypeOf(this, superType)
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 =
KotlinBuiltIns.isNothingOrNullableNothing(this) || this.isDynamic() || this.isCaptured()
KotlinBuiltIns.isNothingOrNullableNothing(this) || this.isDynamic() || this.isCaptured()
fun TypeProjection.substitute(doSubstitute: (KotlinType) -> KotlinType): TypeProjection {
return if (isStarProjection)
@@ -104,7 +116,7 @@ fun List<KotlinType>.defaultProjections(): List<TypeProjection> = map(::TypeProj
fun KotlinType.isDefaultBound(): Boolean = KotlinBuiltIns.isDefaultBound(getSupertypeRepresentative())
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> {
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> =
types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() }
types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() }
fun constituentTypes(types: Collection<KotlinType>): Collection<KotlinType> {
val result = hashSetOf<KotlinType>()
@@ -133,15 +145,14 @@ fun constituentTypes(types: Collection<KotlinType>): Collection<KotlinType> {
}
fun KotlinType.constituentTypes(): Collection<KotlinType> =
constituentTypes(listOf(this))
constituentTypes(listOf(this))
private fun constituentTypes(result: MutableSet<KotlinType>, types: Collection<KotlinType>) {
result.addAll(types)
for (type in types) {
if (type.isFlexible()) {
with (type.asFlexibleType()) { constituentTypes(result, setOf(lowerBound, upperBound)) }
}
else {
with(type.asFlexibleType()) { constituentTypes(result, setOf(lowerBound, upperBound)) }
} else {
constituentTypes(result, type.arguments.mapNotNull { if (!it.isStarProjection) it.type else null })
}
}
@@ -163,8 +174,8 @@ fun KotlinType.replaceArgumentsWithStarProjections(): KotlinType {
val unwrapped = unwrap()
return when (unwrapped) {
is FlexibleType -> KotlinTypeFactory.flexibleType(
unwrapped.lowerBound.replaceArgumentsWithStarProjections(),
unwrapped.upperBound.replaceArgumentsWithStarProjections()
unwrapped.lowerBound.replaceArgumentsWithStarProjections(),
unwrapped.upperBound.replaceArgumentsWithStarProjections()
)
is SimpleType -> unwrapped.replaceArgumentsWithStarProjections()
}.inheritEnhancement(unwrapped)
@@ -179,24 +190,24 @@ private fun SimpleType.replaceArgumentsWithStarProjections(): SimpleType {
}
fun KotlinType.containsTypeAliasParameters(): Boolean =
contains {
it.constructor.declarationDescriptor?.isTypeAliasParameter() ?: false
}
contains {
it.constructor.declarationDescriptor?.isTypeAliasParameter() ?: false
}
fun KotlinType.containsTypeAliases(): Boolean =
contains {
it.constructor.declarationDescriptor is TypeAliasDescriptor
}
contains {
it.constructor.declarationDescriptor is TypeAliasDescriptor
}
fun ClassifierDescriptor.isTypeAliasParameter(): Boolean =
this is TypeParameterDescriptor && containingDeclaration is TypeAliasDescriptor
this is TypeParameterDescriptor && containingDeclaration is TypeAliasDescriptor
fun KotlinType.requiresTypeAliasExpansion(): Boolean =
contains {
it.constructor.declarationDescriptor?.let {
it is TypeAliasDescriptor || it is TypeParameterDescriptor
} ?: false
}
contains {
it.constructor.declarationDescriptor?.let {
it is TypeAliasDescriptor || it is TypeParameterDescriptor
} ?: false
}
fun KotlinType.containsTypeProjectionsInTopLevelArguments(): Boolean {
if (isError) return false
@@ -205,6 +216,6 @@ fun KotlinType.containsTypeProjectionsInTopLevelArguments(): Boolean {
}
fun UnwrappedType.canHaveUndefinedNullability(): Boolean =
constructor is NewTypeVariableConstructor ||
constructor.declarationDescriptor is TypeParameterDescriptor ||
this is NewCapturedType
constructor is NewTypeVariableConstructor ||
constructor.declarationDescriptor is TypeParameterDescriptor ||
this is NewCapturedType