JS: fixed Double.NaN behaviour (KT-13610).
This commit is contained in:
@@ -23,6 +23,10 @@ Kotlin.equals = function (obj1, obj2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj1 !== obj1) {
|
||||
return obj2 !== obj2;
|
||||
}
|
||||
|
||||
if (typeof obj1 == "object" && typeof obj1.equals === "function") {
|
||||
return obj1.equals(obj2);
|
||||
}
|
||||
|
||||
+7
-7
@@ -9603,13 +9603,7 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
|
||||
@TestMetadata("dataClass.kt")
|
||||
public void testDataClass() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ieee754/dataClass.kt");
|
||||
try {
|
||||
doTest(fileName);
|
||||
}
|
||||
catch (Throwable ignore) {
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Looks like this test can be unmuted. Remove IGNORE_BACKEND directive for that.");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("equalsDouble.kt")
|
||||
@@ -9624,6 +9618,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("equalsNaN.kt")
|
||||
public void testEqualsNaN() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ieee754/equalsNaN.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("equalsNullableDouble.kt")
|
||||
public void testEqualsNullableDouble() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ieee754/equalsNullableDouble.kt");
|
||||
|
||||
@@ -190,11 +190,7 @@ public final class Translation {
|
||||
JsNode jsNode = translateExpression(expression, context, block);
|
||||
if (jsNode instanceof JsExpression) {
|
||||
KotlinType expressionType = context.bindingContext().getType(expression);
|
||||
if (expressionType != null && KotlinBuiltIns.isCharOrNullableChar(expressionType) &&
|
||||
(jsNode instanceof JsInvocation || jsNode instanceof JsNameRef || jsNode instanceof JsArrayAccess)) {
|
||||
jsNode = JsAstUtils.boxedCharToChar((JsExpression) jsNode);
|
||||
}
|
||||
return (JsExpression) jsNode;
|
||||
return unboxIfNeeded((JsExpression) jsNode, expressionType != null && KotlinBuiltIns.isCharOrNullableChar(expressionType));
|
||||
}
|
||||
|
||||
assert jsNode instanceof JsStatement : "Unexpected node of type: " + jsNode.getClass().toString();
|
||||
@@ -209,6 +205,16 @@ public final class Translation {
|
||||
return JsLiteral.NULL;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static JsExpression unboxIfNeeded(@NotNull JsExpression expression, boolean charOrNullableChar) {
|
||||
if (charOrNullableChar &&
|
||||
(expression instanceof JsInvocation || expression instanceof JsNameRef || expression instanceof JsArrayAccess)
|
||||
) {
|
||||
expression = JsAstUtils.boxedCharToChar(expression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static JsStatement translateAsStatement(@NotNull KtExpression expression, @NotNull TranslationContext context) {
|
||||
return translateAsStatement(expression, context, context.dynamicContext().jsBlock());
|
||||
|
||||
+54
-27
@@ -16,23 +16,26 @@
|
||||
|
||||
package org.jetbrains.kotlin.js.translate.intrinsic.operation
|
||||
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.builtins.PrimitiveType
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.js.backend.ast.JsBinaryOperation
|
||||
import org.jetbrains.kotlin.js.backend.ast.JsBinaryOperator
|
||||
import org.jetbrains.kotlin.js.backend.ast.JsExpression
|
||||
import org.jetbrains.kotlin.js.backend.ast.JsLiteral
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.js.patterns.NamePredicate
|
||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext
|
||||
import org.jetbrains.kotlin.js.translate.general.Translation
|
||||
import org.jetbrains.kotlin.js.translate.intrinsic.functions.factories.TopLevelFIF
|
||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
||||
import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils
|
||||
import org.jetbrains.kotlin.js.translate.utils.PsiUtils.getOperationToken
|
||||
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.KtBinaryExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.getDataFlowInfoBefore
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowValueFactory
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.expressions.OperatorConventions
|
||||
@@ -41,13 +44,26 @@ import java.util.*
|
||||
|
||||
object EqualsBOIF : BinaryOperationIntrinsicFactory {
|
||||
private object EqualsIntrinsic : AbstractBinaryOperationIntrinsic() {
|
||||
|
||||
// Primitives with no boxing or tricky NaN behavior, represented by a Number at runtime.
|
||||
// Whenever the left side of equality is of this type, equality could be tested via '==='.
|
||||
private val SIMPLE_PRIMITIVES = EnumSet.of(PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.INT)
|
||||
|
||||
override fun apply(expression: KtBinaryExpression, left: JsExpression, right: JsExpression, context: TranslationContext): JsExpression {
|
||||
val isNegated = expression.isNegated()
|
||||
if (right == JsLiteral.NULL || left == JsLiteral.NULL) {
|
||||
return TranslationUtils.nullCheck(if (right == JsLiteral.NULL) left else right, isNegated)
|
||||
}
|
||||
else if (canUseSimpleEquals(expression, context)) {
|
||||
return JsBinaryOperation(if (isNegated) JsBinaryOperator.REF_NEQ else JsBinaryOperator.REF_EQ, left, right)
|
||||
|
||||
val ktLeft = checkNotNull(expression.left) { "No left-hand side: " + expression.text }
|
||||
val ktRight = checkNotNull(expression.right) { "No right-hand side: " + expression.text }
|
||||
val leftType = getRefinedType(ktLeft, context)?.let { KotlinBuiltIns.getPrimitiveTypeByKotlinType(it) }
|
||||
val rightType = getRefinedType(ktRight, context)?.let { KotlinBuiltIns.getPrimitiveTypeByKotlinType(it) }
|
||||
|
||||
if (leftType != null && (leftType in SIMPLE_PRIMITIVES || leftType == rightType && leftType != PrimitiveType.LONG)) {
|
||||
return JsBinaryOperation(if (isNegated) JsBinaryOperator.REF_NEQ else JsBinaryOperator.REF_EQ,
|
||||
Translation.unboxIfNeeded(left, leftType == PrimitiveType.CHAR),
|
||||
Translation.unboxIfNeeded(right, rightType == PrimitiveType.CHAR))
|
||||
}
|
||||
|
||||
val resolvedCall = expression.getResolvedCall(context.bindingContext())
|
||||
@@ -61,15 +77,41 @@ object EqualsBOIF : BinaryOperationIntrinsicFactory {
|
||||
return JsBinaryOperation(if (isNegated) JsBinaryOperator.NEQ else JsBinaryOperator.EQ, left, right)
|
||||
}
|
||||
|
||||
val result = TopLevelFIF.KOTLIN_EQUALS.apply(left, Arrays.asList<JsExpression>(right), context)
|
||||
val maybeBoxedLeft = if (leftType == PrimitiveType.CHAR) JsAstUtils.charToBoxedChar(left) else left
|
||||
val maybeBoxedRight = if (rightType == PrimitiveType.CHAR) JsAstUtils.charToBoxedChar(right) else right
|
||||
|
||||
val result = TopLevelFIF.KOTLIN_EQUALS.apply(maybeBoxedLeft, Arrays.asList<JsExpression>(maybeBoxedRight), context)
|
||||
return if (isNegated) JsAstUtils.not(result) else result
|
||||
}
|
||||
|
||||
private fun canUseSimpleEquals(expression: KtBinaryExpression, context: TranslationContext): Boolean {
|
||||
val left = expression.left
|
||||
assert(left != null) { "No left-hand side: " + expression.text }
|
||||
val typeName = JsDescriptorUtils.getNameIfStandardType(left!!, context)
|
||||
return typeName != null && NamePredicate.PRIMITIVE_NUMBERS_MAPPED_TO_PRIMITIVE_JS.apply(typeName)
|
||||
/**
|
||||
* Tries to get as precise statically known primitive type as possible - taking smart-casts and generic supertypes into account.
|
||||
* This is needed to be compatible with JVM NaN behaviour, in particular the following two cases.
|
||||
*
|
||||
* // Smart-casts
|
||||
* val a: Any = Double.NaN
|
||||
* println(a == a) // true
|
||||
* if (a is Double) {
|
||||
* println(a == a) // false
|
||||
* }
|
||||
*
|
||||
* // Generics with Double super-type
|
||||
* fun <T: Double> foo(v: T) = println(v == v)
|
||||
* foo(Double.NaN) // false
|
||||
*
|
||||
* Also see org/jetbrains/kotlin/codegen/codegenUtil.kt#calcTypeForIEEE754ArithmeticIfNeeded
|
||||
*/
|
||||
private fun getRefinedType(expression: KtExpression, context: TranslationContext): KotlinType? {
|
||||
val bindingContext = context.bindingContext()
|
||||
val descriptor = context.declarationDescriptor ?: context.currentModule
|
||||
val ktType = bindingContext.getType(expression) ?: return null
|
||||
|
||||
val dataFlow = DataFlowValueFactory.createDataFlowValue(expression, ktType, bindingContext, descriptor)
|
||||
val isPrimitiveFn = KotlinBuiltIns::isPrimitiveTypeOrNullablePrimitiveType
|
||||
|
||||
return bindingContext.getDataFlowInfoBefore(expression).getStableTypes(dataFlow).find(isPrimitiveFn) ?: // Smart-casts
|
||||
TypeUtils.getAllSupertypes(ktType).find(isPrimitiveFn) ?: // Generic super-types
|
||||
ktType // Default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,21 +134,6 @@ object EqualsBOIF : BinaryOperationIntrinsicFactory {
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (result != null && leftType != rightType) {
|
||||
val leftChar = (leftType != null && KotlinBuiltIns.isCharOrNullableChar(leftType))
|
||||
val rightChar = (rightType != null && KotlinBuiltIns.isCharOrNullableChar(rightType))
|
||||
|
||||
if (leftChar xor rightChar) {
|
||||
return object : AbstractBinaryOperationIntrinsic() {
|
||||
override fun apply(expression: KtBinaryExpression, left: JsExpression, right: JsExpression, context: TranslationContext): JsExpression {
|
||||
val maybeBoxedLeft = if (leftChar) JsAstUtils.charToBoxedChar(left) else left
|
||||
val maybeBoxedRight = if (rightChar) JsAstUtils.charToBoxedChar(right) else right
|
||||
return result.apply(expression, maybeBoxedLeft, maybeBoxedRight, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
|
||||
import org.jetbrains.kotlin.descriptors.*;
|
||||
import org.jetbrains.kotlin.js.descriptorUtils.DescriptorUtilsKt;
|
||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
||||
import org.jetbrains.kotlin.name.Name;
|
||||
import org.jetbrains.kotlin.psi.KtExpression;
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils;
|
||||
import org.jetbrains.kotlin.resolve.calls.tasks.DynamicCallsKt;
|
||||
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver;
|
||||
@@ -152,12 +148,6 @@ public final class JsDescriptorUtils {
|
||||
!ModalityKt.isOverridableOrOverrides(propertyDescriptor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Name getNameIfStandardType(@NotNull KtExpression expression, @NotNull TranslationContext context) {
|
||||
KotlinType type = context.bindingContext().getType(expression);
|
||||
return type != null ? DescriptorUtilsKt.getNameIfStandardType(type) : null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getModuleName(@NotNull DeclarationDescriptor descriptor) {
|
||||
ModuleDescriptor moduleDescriptor = DescriptorUtils.getContainingModule(findRealInlineDeclaration(descriptor));
|
||||
|
||||
@@ -22,12 +22,11 @@ fun box(): String {
|
||||
mapWithDoubleKeys[1.0] = 1
|
||||
assertEquals("number", jsTypeOf (mapWithDoubleKeys.keys.iterator().next()), "mapWithDoubleKeys")
|
||||
|
||||
// boxed NaNs are not equal, KT-13610
|
||||
// mapWithDoubleKeys.clear()
|
||||
// var dNaN = 0.0 / 0.0
|
||||
// mapWithDoubleKeys[dNaN] = 100
|
||||
// assertEquals(100, mapWithDoubleKeys[dNaN])
|
||||
// assertEquals("number", jsTypeOf (mapWithDoubleKeys.keys.iterator().next()), "dNaN")
|
||||
mapWithDoubleKeys.clear()
|
||||
var dNaN = 0.0 / 0.0
|
||||
mapWithDoubleKeys[dNaN] = 100
|
||||
assertEquals(100, mapWithDoubleKeys[dNaN])
|
||||
assertEquals("number", jsTypeOf (mapWithDoubleKeys.keys.iterator().next()), "dNaN")
|
||||
|
||||
mapWithDoubleKeys.clear()
|
||||
var dPositiveInfinity = +1.0 / 0.0
|
||||
@@ -45,12 +44,11 @@ fun box(): String {
|
||||
mapWithFloatKeys[1.0f] = 1
|
||||
assertEquals("number", jsTypeOf (mapWithFloatKeys.keys.iterator().next()), "mapWithFloatKeys")
|
||||
|
||||
// boxed NaNs are not equal, KT-13610
|
||||
// mapWithFloatKeys.clear()
|
||||
// var fNaN: Float = 0.0f / 0.0f
|
||||
// mapWithFloatKeys[fNaN] = 100
|
||||
// assertEquals(100, mapWithFloatKeys[fNaN])
|
||||
// assertEquals("number", jsTypeOf (mapWithFloatKeys.keys.iterator().next()), "fNaN")
|
||||
mapWithFloatKeys.clear()
|
||||
var fNaN: Float = 0.0f / 0.0f
|
||||
mapWithFloatKeys[fNaN] = 100
|
||||
assertEquals(100, mapWithFloatKeys[fNaN])
|
||||
assertEquals("number", jsTypeOf (mapWithFloatKeys.keys.iterator().next()), "fNaN")
|
||||
|
||||
mapWithFloatKeys.clear()
|
||||
var fPositiveInfinity = +1.0f / 0.0f
|
||||
|
||||
Reference in New Issue
Block a user