From 9e5ecc11b7b4f88f4c99a14503eb97dba6f71d09 Mon Sep 17 00:00:00 2001 From: Anton Bannykh Date: Mon, 27 Feb 2017 18:44:50 +0300 Subject: [PATCH] JS: fixed Double.NaN behaviour (KT-13610). --- .../testData/codegen/box/ieee754/dataClass.kt | 1 - .../testData/codegen/box/ieee754/equalsNaN.kt | 223 ++++++++++++++++++ .../light-analysis/ieee754/equalsNaN.txt | 24 ++ .../ir/IrBlackBoxCodegenTestGenerated.java | 6 + .../codegen/BlackBoxCodegenTestGenerated.java | 6 + .../kotlin/builtins/KotlinBuiltIns.java | 13 +- js/js.libraries/src/js/core.js | 4 + .../semantics/JsCodegenBoxTestGenerated.java | 14 +- .../js/translate/general/Translation.java | 16 +- .../intrinsic/operation/EqualsBOIF.kt | 81 ++++--- .../js/translate/utils/JsDescriptorUtils.java | 10 - .../standardClasses/hashMapTypeOfElement.kt | 22 +- 12 files changed, 357 insertions(+), 63 deletions(-) create mode 100644 compiler/testData/codegen/box/ieee754/equalsNaN.kt create mode 100644 compiler/testData/codegen/light-analysis/ieee754/equalsNaN.txt diff --git a/compiler/testData/codegen/box/ieee754/dataClass.kt b/compiler/testData/codegen/box/ieee754/dataClass.kt index 415c6814408..8f05b07f453 100644 --- a/compiler/testData/codegen/box/ieee754/dataClass.kt +++ b/compiler/testData/codegen/box/ieee754/dataClass.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JS data class Test(val z1: Double, val z2: Double?) fun box(): String { diff --git a/compiler/testData/codegen/box/ieee754/equalsNaN.kt b/compiler/testData/codegen/box/ieee754/equalsNaN.kt new file mode 100644 index 00000000000..3a396bd432d --- /dev/null +++ b/compiler/testData/codegen/box/ieee754/equalsNaN.kt @@ -0,0 +1,223 @@ +// WITH_RUNTIME + +import kotlin.test.* + +object O { + var equalsCalled: Boolean = false + get(): Boolean { + val result = field + field = false + return result + } + set(v: Boolean) { + field = v + } + + override fun equals(a: Any?): Boolean { + equalsCalled = true + return true + } +} + +val A: Any = O + +fun testDouble(d: Double, v: T, vararg va: T) { + assertFalse(d == d, "Double: d != d") + assertFalse(d == v, "Double: d != v") + assertFalse(d == va[0], "Double: d != va[0]") + assertFalse(v == d, "Double: v != d") + assertFalse(v == v, "Double: v != v") + assertFalse(v == va[0], "Double: v != va[0]") + assertFalse(va[0] == d, "Double: va[0] != d") + assertFalse(va[0] == v, "Double: va[0] != v") + assertFalse(va[0] == va[0], "Double: va[0] != va[0]") + + assertTrue(d != d, "Double: d == d") + assertTrue(d != v, "Double: d == v") + assertTrue(d != va[0], "Double: d == va[0]") + assertTrue(v != d, "Double: v == d") + assertTrue(v != v, "Double: v == v") + assertTrue(v != va[0], "Double: v == va[0]") + assertTrue(va[0] != d, "Double: va[0] == d") + assertTrue(va[0] != v, "Double: va[0] == v") + assertTrue(va[0] != va[0], "Double: va[0] == va[0]") +} + +fun testFloat(d: Float, v: T, vararg va: T) { + assertFalse(d == d, "Float: d != d") + assertFalse(d == v, "Float: d != v") + assertFalse(d == va[0], "Float: d != va[0]") + assertFalse(v == d, "Float: v != d") + assertFalse(v == v, "Float: v != v") + assertFalse(v == va[0], "Float: v != va[0]") + assertFalse(va[0] == d, "Float: va[0] != d") + assertFalse(va[0] == v, "Float: va[0] != v") + assertFalse(va[0] == va[0], "Float: va[0] != va[0]") + + assertTrue(d != d, "Float: d == d") + assertTrue(d != v, "Float: d == v") + assertTrue(d != va[0], "Float: d == va[0]") + assertTrue(v != d, "Float: v == d") + assertTrue(v != v, "Float: v == v") + assertTrue(v != va[0], "Float: v == va[0]") + assertTrue(va[0] != d, "Float: va[0] == d") + assertTrue(va[0] != v, "Float: va[0] == v") + assertTrue(va[0] != va[0], "Float: va[0] == va[0]") +} + +var gdn: Any = Double.NaN +var gfn: Any = Float.NaN + +fun box(): String { + + // Double + + val dn = Double.NaN + val adn: Any = dn + val dnq: Double? = dn + val adnq: Any? = dn + + assertFalse(dn == dn, "Double: NaN == NaN") + assertTrue(dn == adn, "Double: NaN != (Any)NaN") + assertTrue(adn == dn, "Double: (Any)NaN != NaN") + assertTrue(adn == adn, "Double: (Any)NaN != (Any)NaN") + + assertFalse(dn == dnq, "Double: NaN == NaN?") + assertTrue(dn == adnq, "Double: NaN != (Any?)NaN") + assertTrue(adn == dnq, "Double: (Any)NaN != NaN?") + assertTrue(adn == adnq, "Double: (Any)NaN != (Any?)NaN") + + assertFalse(dnq == dn, "Double: NaN? == NaN") + assertTrue(dnq == adn, "Double: NaN? != (Any)NaN") + assertTrue(adnq == dn, "Double: (Any?)NaN != NaN") + assertTrue(adnq == adn, "Double: (Any?)NaN != (Any)NaN") + + assertFalse(dnq == dnq, "Double: NaN? == NaN?") + assertTrue(dnq == adnq, "Double: NaN? != (Any?)NaN") + assertTrue(adnq == dnq, "Double: (Any?)NaN != NaN?") + assertTrue(adnq == adnq, "Double: (Any?)NaN != (Any?)NaN") + + assertTrue(dn != dn, "Double: NaN == NaN") + assertFalse(dn != adn, "Double: NaN != (Any)NaN") + assertFalse(adn != dn, "Double: (Any)NaN != NaN") + assertFalse(adn != adn, "Double: (Any)NaN != (Any)NaN") + + assertTrue(dn != dnq, "Double: NaN == NaN?") + assertFalse(dn != adnq, "Double: NaN != (Any?)NaN") + assertFalse(adn != dnq, "Double: (Any)NaN != NaN?") + assertFalse(adn != adnq, "Double: (Any)NaN != (Any?)NaN") + + assertTrue(dnq != dn, "Double: NaN? == NaN") + assertFalse(dnq != adn, "Double: NaN? != (Any)NaN") + assertFalse(adnq != dn, "Double: (Any?)NaN != NaN") + assertFalse(adnq != adn, "Double: (Any?)NaN != (Any)NaN") + + assertTrue(dnq != dnq, "Double: NaN? == NaN?") + assertFalse(dnq != adnq, "Double: NaN? != (Any?)NaN") + assertFalse(adnq != dnq, "Double: (Any?)NaN != NaN?") + assertFalse(adnq != adnq, "Double: (Any?)NaN != (Any?)NaN") + + // Stable smart-casts + if (adn is Double) { + assertFalse(adn == adn, "Double smart-cast: NaN == NaN") + assertTrue(adn != adn, "Double smart-cast: NaN == NaN") + } + if (adnq is Double?) { + assertFalse(adnq == adnq, "Double? smart-cast: NaN? == NaN?") + assertTrue(adnq != adnq, "Double? smart-cast: NaN? == NaN?") + } + // Unstable smart-casts + if (gdn is Double) { + assertTrue(gdn == gdn, "Unstable Double smart-cast: NaN != NaN") + assertFalse(gdn != gdn, "Unstable Double smart-cast: NaN != NaN") + } + if (gdn is Double?) { + assertTrue(gdn == gdn, "Unstable Double smart-cast: NaN != NaN") + assertFalse(gdn != gdn, "Unstable Double smart-cast: NaN != NaN") + } + + // Explicit .equals + assertTrue(A == dn && O.equalsCalled, "A.equals not called for A == dn") + assertTrue(dn != A && !O.equalsCalled, "A.equals called for dn == A") + assertFalse(A != dn || !O.equalsCalled, "A.equals not called for A != dn") + assertFalse(dn == A || O.equalsCalled, "A.equals called for dn != A") + + // Generics and varags + testDouble(Double.NaN, Double.NaN, Double.NaN) + + // Float + + val fn = Float.NaN + val afn: Any = fn + val fnq: Float? = fn + val afnq: Any? = fn + + assertFalse(fn == fn, "Float: NaN == NaN") + assertTrue(fn == afn, "Float: NaN != (Any)NaN") + assertTrue(afn == fn, "Float: (Any)NaN != NaN") + assertTrue(afn == afn, "Float: (Any)NaN != (Any)NaN") + + assertFalse(fn == fnq, "Float: NaN == NaN?") + assertTrue(fn == afnq, "Float: NaN != (Any?)NaN") + assertTrue(afn == fnq, "Float: (Any)NaN != NaN?") + assertTrue(afn == afnq, "Float: (Any)NaN != (Any?)NaN") + + assertFalse(fnq == fn, "Float: NaN? == NaN") + assertTrue(fnq == afn, "Float: NaN? != (Any)NaN") + assertTrue(afnq == fn, "Float: (Any?)NaN != NaN") + assertTrue(afnq == afn, "Float: (Any?)NaN != (Any)NaN") + + assertFalse(fnq == fnq, "Float: NaN? == NaN?") + assertTrue(fnq == afnq, "Float: NaN? != (Any?)NaN") + assertTrue(afnq == fnq, "Float: (Any?)NaN != NaN?") + assertTrue(afnq == afnq, "Float: (Any?)NaN != (Any?)NaN") + + assertTrue(fn != fn, "Float: NaN == NaN") + assertFalse(fn != afn, "Float: NaN != (Any)NaN") + assertFalse(afn != fn, "Float: (Any)NaN != NaN") + assertFalse(afn != afn, "Float: (Any)NaN != (Any)NaN") + + assertTrue(fn != fnq, "Float: NaN == NaN?") + assertFalse(fn != afnq, "Float: NaN != (Any?)NaN") + assertFalse(afn != fnq, "Float: (Any)NaN != NaN?") + assertFalse(afn != afnq, "Float: (Any)NaN != (Any?)NaN") + + assertTrue(fnq != fn, "Float: NaN? == NaN") + assertFalse(fnq != afn, "Float: NaN? != (Any)NaN") + assertFalse(afnq != fn, "Float: (Any?)NaN != NaN") + assertFalse(afnq != afn, "Float: (Any?)NaN != (Any)NaN") + + assertTrue(fnq != fnq, "Float: NaN? == NaN?") + assertFalse(fnq != afnq, "Float: NaN? != (Any?)NaN") + assertFalse(afnq != fnq, "Float: (Any?)NaN != NaN?") + assertFalse(afnq != afnq, "Float: (Any?)NaN != (Any?)NaN") + + // Stable smart-casts + if (afn is Float) { + assertFalse(afn == afn, "Float smart-cast: NaN == NaN") + assertTrue(afn != afn, "Float smart-cast: NaN == NaN") + } + if (afnq is Float?) { + assertFalse(afnq == afnq, "Float? smart-cast: NaN? == NaN?") + assertTrue(afnq != afnq, "Float? smart-cast: NaN? == NaN?") + } + // Unstable smart-casts + if (gfn is Float) { + assertTrue(gfn == gfn, "Unstable Float smart-cast: NaN != NaN") + assertFalse(gfn != gfn, "Unstable Float smart-cast: NaN != NaN") + } + if (gfn is Float?) { + assertTrue(gfn == gfn, "Unstable Float smart-cast: NaN != NaN") + assertFalse(gfn != gfn, "Unstable Float smart-cast: NaN != NaN") + } + + assertTrue(A == fn && O.equalsCalled, "A.equals not called for A == fn") + assertTrue(fn != A && !O.equalsCalled, "A.equals called for fn == A") + assertFalse(A != fn || !O.equalsCalled, "A.equals not called for A != fn") + assertFalse(fn == A || O.equalsCalled, "A.equals called for fn != A") + + // Generics and varags + testFloat(Float.NaN, Float.NaN, Float.NaN) + + return "OK" +} \ No newline at end of file diff --git a/compiler/testData/codegen/light-analysis/ieee754/equalsNaN.txt b/compiler/testData/codegen/light-analysis/ieee754/equalsNaN.txt new file mode 100644 index 00000000000..830ff6baf8c --- /dev/null +++ b/compiler/testData/codegen/light-analysis/ieee754/equalsNaN.txt @@ -0,0 +1,24 @@ +@kotlin.Metadata +public final class EqualsNaNKt { + private final static @org.jetbrains.annotations.NotNull field A: java.lang.Object + private static @org.jetbrains.annotations.NotNull field gdn: java.lang.Object + private static @org.jetbrains.annotations.NotNull field gfn: java.lang.Object + public final static @org.jetbrains.annotations.NotNull method box(): java.lang.String + public final static @org.jetbrains.annotations.NotNull method getA(): java.lang.Object + public final static @org.jetbrains.annotations.NotNull method getGdn(): java.lang.Object + public final static @org.jetbrains.annotations.NotNull method getGfn(): java.lang.Object + public final static method setGdn(@org.jetbrains.annotations.NotNull p0: java.lang.Object): void + public final static method setGfn(@org.jetbrains.annotations.NotNull p0: java.lang.Object): void + public final static method testDouble(p0: double, p1: double, @org.jetbrains.annotations.NotNull p2: java.lang.Double[]): void + public final static method testFloat(p0: float, p1: float, @org.jetbrains.annotations.NotNull p2: java.lang.Float[]): void +} + +@kotlin.Metadata +public final class O { + public final static field INSTANCE: O + private static field equalsCalled: boolean + private method (): void + public method equals(@org.jetbrains.annotations.Nullable p0: java.lang.Object): boolean + public final method getEqualsCalled(): boolean + public final method setEqualsCalled(p0: boolean): void +} diff --git a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java index 2e648cffd9b..edb9a8c37ec 100644 --- a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java @@ -8525,6 +8525,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes 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"); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 3393a3c37ca..dabaffefe63 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -8525,6 +8525,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { 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"); diff --git a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java index 81089ee913d..95269485f39 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java +++ b/core/descriptors/src/org/jetbrains/kotlin/builtins/KotlinBuiltIns.java @@ -878,8 +878,19 @@ public abstract class KotlinBuiltIns { } public static boolean isPrimitiveType(@NotNull KotlinType type) { + return !type.isMarkedNullable() && isPrimitiveTypeOrNullablePrimitiveType(type); + } + + public static boolean isPrimitiveTypeOrNullablePrimitiveType(@NotNull KotlinType type) { ClassifierDescriptor descriptor = type.getConstructor().getDeclarationDescriptor(); - return !type.isMarkedNullable() && descriptor instanceof ClassDescriptor && isPrimitiveClass((ClassDescriptor) descriptor); + return descriptor instanceof ClassDescriptor && isPrimitiveClass((ClassDescriptor) descriptor); + } + + @Nullable + public static PrimitiveType getPrimitiveTypeByKotlinType(@NotNull KotlinType type) { + ClassifierDescriptor descriptor = type.getConstructor().getDeclarationDescriptor(); + if (descriptor == null) return null; + return getPrimitiveTypeByFqName(getFqName(descriptor)); } public static boolean isPrimitiveClass(@NotNull ClassDescriptor descriptor) { diff --git a/js/js.libraries/src/js/core.js b/js/js.libraries/src/js/core.js index f52bf610c3e..6c51f8d6bf5 100644 --- a/js/js.libraries/src/js/core.js +++ b/js/js.libraries/src/js/core.js @@ -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); } diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java index 7c77f7555fb..a3b95b376ed 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java @@ -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"); diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java index 3894d649f47..bfa37c7291e 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java @@ -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()); diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/intrinsic/operation/EqualsBOIF.kt b/js/js.translator/src/org/jetbrains/kotlin/js/translate/intrinsic/operation/EqualsBOIF.kt index 018ce022ef2..e62e6440600 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/intrinsic/operation/EqualsBOIF.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/intrinsic/operation/EqualsBOIF.kt @@ -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(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(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 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 } diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsDescriptorUtils.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsDescriptorUtils.java index 987d660f8ef..2813753f935 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsDescriptorUtils.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/utils/JsDescriptorUtils.java @@ -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)); diff --git a/js/js.translator/testData/box/standardClasses/hashMapTypeOfElement.kt b/js/js.translator/testData/box/standardClasses/hashMapTypeOfElement.kt index f16083883ef..8197175cf95 100644 --- a/js/js.translator/testData/box/standardClasses/hashMapTypeOfElement.kt +++ b/js/js.translator/testData/box/standardClasses/hashMapTypeOfElement.kt @@ -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