From 569574b7bffd3ade312d9f848e3fa14f442a1bef Mon Sep 17 00:00:00 2001 From: "lrn@chromium.org" Date: Tue, 3 May 2011 12:15:14 +0000 Subject: [PATCH] Fix implementation of == to correctly convert Date objects to primitives. Fix issue 1356 BUG=v8:1356 TEST=mjsunit/double-equals Review URL: http://codereview.chromium.org/6912021 git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@7761 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- src/runtime.js | 50 ++++--- test/mjsunit/double-equals.js | 250 +++++++++++++++++++++++++--------- 2 files changed, 214 insertions(+), 86 deletions(-) diff --git a/src/runtime.js b/src/runtime.js index 66d839bec..4f53efe0b 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -49,41 +49,47 @@ const $Function = global.Function; const $Boolean = global.Boolean; const $NaN = 0/0; - -// ECMA-262, section 11.9.1, page 55. +// ECMA-262 Section 11.9.3. function EQUALS(y) { if (IS_STRING(this) && IS_STRING(y)) return %StringEquals(this, y); var x = this; - // NOTE: We use iteration instead of recursion, because it is - // difficult to call EQUALS with the correct setting of 'this' in - // an efficient way. while (true) { if (IS_NUMBER(x)) { - if (y == null) return 1; // not equal - return %NumberEquals(x, %ToNumber(y)); + while (true) { + if (IS_NUMBER(y)) return %NumberEquals(x, y); + if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal + if (!IS_SPEC_OBJECT(y)) { + // String or boolean. + return %NumberEquals(x, %ToNumber(y)); + } + y = %ToPrimitive(y, NO_HINT); + } } else if (IS_STRING(x)) { - if (IS_STRING(y)) return %StringEquals(x, y); + while (true) { + if (IS_STRING(y)) return %StringEquals(x, y); + if (IS_NUMBER(y)) return %NumberEquals(%ToNumber(x), y); + if (IS_BOOLEAN(y)) return %NumberEquals(%ToNumber(x), %ToNumber(y)); + if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal + y = %ToPrimitive(y, NO_HINT); + } + } else if (IS_BOOLEAN(x)) { + if (IS_BOOLEAN(y)) return %_ObjectEquals(x, y) ? 0 : 1; + if (IS_NULL_OR_UNDEFINED(y)) return 1; if (IS_NUMBER(y)) return %NumberEquals(%ToNumber(x), y); - if (IS_BOOLEAN(y)) return %NumberEquals(%ToNumber(x), %ToNumber(y)); - if (y == null) return 1; // not equal + if (IS_STRING(y)) return %NumberEquals(%ToNumber(x), %ToNumber(y)); + // y is object. + x = %ToNumber(x); y = %ToPrimitive(y, NO_HINT); - } else if (IS_BOOLEAN(x)) { - if (IS_BOOLEAN(y)) { - return %_ObjectEquals(x, y) ? 0 : 1; - } - if (y == null) return 1; // not equal - return %NumberEquals(%ToNumber(x), %ToNumber(y)); - } else if (x == null) { - // NOTE: This checks for both null and undefined. - return (y == null) ? 0 : 1; + } else if (IS_NULL_OR_UNDEFINED(x)) { + return IS_NULL_OR_UNDEFINED(y) ? 0 : 1; } else { - // x is not a number, boolean, null or undefined. - if (y == null) return 1; // not equal + // x is an object. if (IS_SPEC_OBJECT(y)) { return %_ObjectEquals(x, y) ? 0 : 1; } - + if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal + if (IS_BOOLEAN(y)) y = %ToNumber(y); x = %ToPrimitive(x, NO_HINT); } } diff --git a/test/mjsunit/double-equals.js b/test/mjsunit/double-equals.js index a68d7eaf9..5ebf92ca7 100644 --- a/test/mjsunit/double-equals.js +++ b/test/mjsunit/double-equals.js @@ -31,84 +31,206 @@ * implementation of assertEquals. */ -assertTrue (void 0 == void 0, "void 0 == void 0"); -assertTrue (null == null, "null == null"); -assertFalse(NaN == NaN, "NaN == NaN"); -assertFalse(NaN == 0, "NaN == 0"); -assertFalse(0 == NaN, "0 == NaN"); -assertFalse(NaN == Infinity, "NaN == Inf"); -assertFalse(Infinity == NaN, "Inf == NaN"); - -assertTrue(Number.MAX_VALUE == Number.MAX_VALUE, "MAX == MAX"); -assertTrue(Number.MIN_VALUE == Number.MIN_VALUE, "MIN == MIN"); -assertTrue(Infinity == Infinity, "Inf == Inf"); -assertTrue(-Infinity == -Infinity, "-Inf == -Inf"); - -assertTrue(0 == 0, "0 == 0"); -assertTrue(0 == -0, "0 == -0"); -assertTrue(-0 == 0, "-0 == 0"); -assertTrue(-0 == -0, "-0 == -0"); - -assertFalse(0.9 == 1, "0.9 == 1"); -assertFalse(0.999999 == 1, "0.999999 == 1"); -assertFalse(0.9999999999 == 1, "0.9999999999 == 1"); -assertFalse(0.9999999999999 == 1, "0.9999999999999 == 1"); - -assertTrue('hello' == 'hello', "'hello' == 'hello'"); - -assertTrue (true == true, "true == true"); -assertTrue (false == false, "false == false"); -assertFalse(true == false, "true == false"); -assertFalse(false == true, "false == true"); - -assertFalse(new Wrapper(null) == new Wrapper(null), "new Wrapper(null) == new Wrapper(null)"); -assertFalse(new Boolean(true) == new Boolean(true), "new Boolean(true) == new Boolean(true)"); -assertFalse(new Boolean(false) == new Boolean(false), "new Boolean(false) == new Boolean(false)"); +function testEqual(a, b) { + assertTrue(a == b); + assertTrue(b == a); + assertFalse(a != b); + assertFalse(b != a); +} + +function testNotEqual(a, b) { + assertFalse(a == b); + assertFalse(b == a); + assertTrue(a != b); + assertTrue(b != a); +} + +// Object where ToPrimitive returns value. +function Wrapper(value) { + this.value = value; + this.valueOf = function () { return this.value; }; +} + +// Object where ToPrimitive returns value by failover to toString when +// valueOf isn't a function. +function Wrapper2(value) { + this.value = value; + this.valueOf = null; + this.toString = function () { return this.value; }; +} + + +// Compare values of same type. + +// Numbers are equal if same, unless NaN, which isn't equal to anything, and +// +/-0 being equal. + +testNotEqual(NaN, NaN); +testNotEqual(NaN, 0); +testNotEqual(NaN, Infinity); + +testEqual(Number.MAX_VALUE, Number.MAX_VALUE); +testEqual(Number.MIN_VALUE, Number.MIN_VALUE); +testEqual(Infinity, Infinity); +testEqual(-Infinity, -Infinity); + +testEqual(0, 0); +testEqual(0, -0); +testEqual(-0, -0); + +testNotEqual(0.9, 1); +testNotEqual(0.999999, 1); +testNotEqual(0.9999999999, 1); +testNotEqual(0.9999999999999, 1); + +// Strings are equal if containing the same code points. + +testEqual('hello', 'hello'); +testEqual('hello', 'hel' + 'lo'); +testEqual('', ''); +testEqual('\u0020\x20', ' '); // Escapes are not part of the value. + +// Booleans are equal if they are the same. + +testEqual(true, true); +testEqual(false, false); +testNotEqual(true, false); + +// Null and undefined are equal to themselves. + +testEqual(null, null); +testEqual(undefined, undefined); + +// Objects are equal if they are the same object only. + +testEqual(Math, Math); +testEqual(Object.prototype, Object.prototype); + (function () { var x = new Wrapper(null); var y = x, z = x; - assertTrue(y == x); + testEqual(y, x); })(); (function () { var x = new Boolean(true); var y = x, z = x; - assertTrue(y == x); + testEqual(y, x); })(); (function () { var x = new Boolean(false); var y = x, z = x; - assertTrue(y == x); + testEqual(y, x); })(); -assertTrue(null == void 0, "null == void 0"); -assertTrue(void 0 == null, "void 0 == null"); -assertFalse(new Wrapper(null) == null, "new Wrapper(null) == null"); -assertFalse(null == new Wrapper(null), "null == new Wrapper(null)"); - -assertTrue(1 == '1', "1 == '1"); -assertTrue(255 == '0xff', "255 == '0xff'"); -assertTrue(0 == '\r', "0 == '\\r'"); -assertTrue(1e19 == '1e19', "1e19 == '1e19'"); - -assertTrue(new Boolean(true) == true, "new Boolean(true) == true"); -assertTrue(new Boolean(false) == false, "new Boolean(false) == false"); -assertTrue(true == new Boolean(true), "true == new Boolean(true)"); -assertTrue(false == new Boolean(false), "false == new Boolean(false)"); - -assertTrue(Boolean(true) == true, "Boolean(true) == true"); -assertTrue(Boolean(false) == false, "Boolean(false) == false"); -assertTrue(true == Boolean(true), "true == Boolean(true)"); -assertTrue(false == Boolean(false), "false == Boolean(false)"); - -assertTrue(new Wrapper(true) == true, "new Wrapper(true) == true"); -assertTrue(new Wrapper(false) == false, "new Wrapper(false) == false"); -assertTrue(true == new Wrapper(true), "true = new Wrapper(true)"); -assertTrue(false == new Wrapper(false), "false = new Wrapper(false)"); - -function Wrapper(value) { - this.value = value; - this.valueOf = function () { return this.value; }; +// Test comparing values of different types. + +// Null and undefined are equal to each-other, and to nothing else. +testEqual(null, undefined); +testEqual(undefined, null); + +testNotEqual(null, new Wrapper(null)); +testNotEqual(null, 0); +testNotEqual(null, false); +testNotEqual(null, ""); +testNotEqual(null, new Object()); +testNotEqual(undefined, new Wrapper(undefined)); +testNotEqual(undefined, 0); +testNotEqual(undefined, false); +testNotEqual(undefined, ""); +testNotEqual(undefined, new Object()); + +// Numbers compared to Strings will convert the string to a number using +// the internal ToNumber conversion. + +testEqual(1, '1'); +testEqual(255, '0xff'); +testEqual(0, '\r'); // ToNumber ignores tailing and trailing whitespace. +testEqual(1e19, '1e19'); +testEqual(Infinity, "Infinity"); + +// Booleans compared to anything else will be converted to numbers. +testEqual(false, 0); +testEqual(true, 1); +testEqual(false, "0"); // String also converted to number. +testEqual(true, "1"); + +// Objects compared to Number or String (or Boolean, since that's converted +// to Number too) is converted to primitive using ToPrimitive with NO HINT. +// Having no hint means Date gets a string hint, and everything else gets +// a number hint. + +testEqual(new Boolean(true), true); +testEqual(new Boolean(true), 1); // First to primtive boolean, then to number. +testEqual(new Boolean(false), false); +testEqual(new Boolean(false), 0); + +testEqual(new Wrapper(true), true); +testEqual(new Wrapper(true), 1); +testEqual(new Wrapper(false), false); +testEqual(new Wrapper(false), 0); + +testEqual(new Wrapper2(true), true); +testEqual(new Wrapper2(true), 1); +testEqual(new Wrapper2(false), false); +testEqual(new Wrapper2(false), 0); + +testEqual(new Number(1), true); +testEqual(new Number(1), 1); +testEqual(new Number(0), false); +testEqual(new Number(0), 0); + +// Date objects convert to string, not number (and the string does not +// convert to the number). +testEqual(new Date(42), String(new Date(42))); +testNotEqual(new Date(42), Number(new Date(42))); +var dnow = new Date(); +testEqual(dnow, dnow); +testEqual(dnow, String(dnow)); +testNotEqual(dnow, Number(dnow)); + +// Doesn't just call toString, but uses ToPrimitive which tries toString first +// and valueOf second. +dnow.toString = null; +testEqual(dnow, Number(dnow)); +dnow.valueOf = function () { return "42"; }; +testEqual(dnow, 42); +dnow.toString = function () { return "1"; }; +testEqual(dnow, true); + + +// Objects compared to other objects, or to null and undefined, are not +// converted to primitive. +testNotEqual(new Wrapper(null), new Wrapper(null)); +testNotEqual(new Boolean(true), new Boolean(true)); +testNotEqual(new Boolean(false), new Boolean(false)); +testNotEqual(new String("a"), new String("a")); +testNotEqual(new Number(42), new Number(42)); +testNotEqual(new Date(42), new Date(42)); +testNotEqual(new Array(42), new Array(42)); +testNotEqual(new Object(), new Object()); + +// Object that can't be converted to primitive. +var badObject = { + valueOf: null, + toString: function() { + return this; // Not primitive. + } +}; + +testEqual(badObject, badObject); +testNotEqual(badObject, {}); +testNotEqual(badObject, null); +testNotEqual(badObject, undefined); +// Forcing conversion will throw. +function testBadConversion(value) { + assertThrows(function() { return badObject == value; }); + assertThrows(function() { return badObject != value; }); + assertThrows(function() { return value == badObject; }); + assertThrows(function() { return value != badObject; }); } +testBadConversion(0); +testBadConversion("string"); +testBadConversion(true); -- 2.34.1