From f4ed4ec9d9dee54bc2caa1e80a75d4a0a09e2c42 Mon Sep 17 00:00:00 2001 From: Dmitry Petrov Date: Thu, 1 Feb 2018 14:11:36 +0300 Subject: [PATCH] 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, 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#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. --- .../kotlin/resolve/BindingContext.java | 3 + .../kotlin/resolve/TargetPlatform.kt | 3 +- .../PrimitiveNumericComparisonCallChecker.kt | 75 +++++++++++++++++++ .../org/jetbrains/kotlin/types/TypeUtils.kt | 61 ++++++++------- 4 files changed, 116 insertions(+), 26 deletions(-) create mode 100644 compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/PrimitiveNumericComparisonCallChecker.kt diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/BindingContext.java b/compiler/frontend/src/org/jetbrains/kotlin/resolve/BindingContext.java index 47f29214141..2bccd237853 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/BindingContext.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/BindingContext.java @@ -267,6 +267,9 @@ public interface BindingContext { WritableSlice NEW_INFERENCE_LAMBDA_INFO = new BasicWritableSlice<>(DO_NOTHING); + WritableSlice PRIMITIVE_NUMERIC_COMPARISON_TYPE = Slices.createSimpleSlice(); + WritableSlice 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); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt index 53a6bcd4da7..514bc1dd583 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt @@ -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() private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf( diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/PrimitiveNumericComparisonCallChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/PrimitiveNumericComparisonCallChecker.kt new file mode 100644 index 00000000000..13c9fcd7c14 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/PrimitiveNumericComparisonCallChecker.kt @@ -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.findPrimitiveType() = + find { it.isPrimitiveNumberOrNullableType() }?.makeNotNullable() +} \ No newline at end of file diff --git a/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt b/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt index 21bb294f90d..7a858d60bb4 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/types/TypeUtils.kt @@ -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.defaultProjections(): List = 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.closure(f: (KotlinType) -> Collection): Collection { if (size == 0) return this @@ -124,7 +136,7 @@ fun Collection.closure(f: (KotlinType) -> Collection): C } fun boundClosure(types: Collection): Collection = - types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() } + types.closure { type -> TypeUtils.getTypeParameterDescriptorOrNull(type)?.upperBounds ?: emptySet() } fun constituentTypes(types: Collection): Collection { val result = hashSetOf() @@ -133,15 +145,14 @@ fun constituentTypes(types: Collection): Collection { } fun KotlinType.constituentTypes(): Collection = - constituentTypes(listOf(this)) + constituentTypes(listOf(this)) private fun constituentTypes(result: MutableSet, types: Collection) { 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