Bug 1130636 - Reimplement Array.prototype.toLocaleString as per ECMA-402, 2nd edition. r=Waldo
authorAndré Bargull <andre.bargull@gmail.com>
Wed, 05 Oct 2016 03:25:57 -0700
changeset 316791 a6506191e8948b9c8e4288df9b6ebbe73e82fba8
parent 316790 18722be58f18558bed9c4c32ae1a15f6f1a7f5f6
child 316792 a045ca98c52cb419e910a6b78b872972e81de9f8
push id32932
push userphilringnalda@gmail.com
push dateFri, 07 Oct 2016 03:24:25 +0000
treeherderautoland@7affb66131bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersWaldo
bugs1130636
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1130636 - Reimplement Array.prototype.toLocaleString as per ECMA-402, 2nd edition. r=Waldo
js/src/builtin/Array.js
js/src/jsarray.cpp
js/src/tests/Intl/Array/shell.js
js/src/tests/Intl/Array/toLocaleString-date.js
js/src/tests/Intl/Array/toLocaleString-number.js
js/src/tests/Intl/Array/toLocaleString.js
js/src/tests/ecma_6/Array/toLocaleString-nointl.js
js/src/vm/CommonPropertyNames.h
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -872,16 +872,71 @@ function ArrayToString() {
     var func = array.join;
 
     // Steps 5-6.
     if (!IsCallable(func))
         return callFunction(std_Object_toString, array);
     return callContentFunction(func, array);
 }
 
+// ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f
+// 22.1.3.27 Array.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ])
+// ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276
+// 13.4.1 Array.prototype.toLocaleString ([ locales [ , options ]])
+function ArrayToLocaleString(locales, options) {
+    // Step 1 (ToObject already performed in native code).
+    assert(IsObject(this), "|this| should be an object");
+    var array = this;
+
+    // Step 2.
+    var len = ToLength(array.length);
+
+    // Step 4.
+    if (len === 0)
+        return "";
+
+    // Step 5.
+    var firstElement = array[0];
+
+    // Steps 6-7.
+    var R;
+    if (firstElement === undefined || firstElement === null) {
+        R = "";
+    } else {
+#if EXPOSE_INTL_API
+        R = ToString(callContentFunction(firstElement.toLocaleString, firstElement, locales, options));
+#else
+        R = ToString(callContentFunction(firstElement.toLocaleString, firstElement));
+#endif
+    }
+
+    // Step 3 (reordered).
+    // We don't (yet?) implement locale-dependent separators.
+    var separator = ",";
+
+    // Steps 8-9.
+    for (var k = 1; k < len; k++) {
+        // Step 9.b.
+        var nextElement = array[k];
+
+        // Steps 9.a, 9.c-e.
+        R += separator;
+        if (!(nextElement === undefined || nextElement === null)) {
+#if EXPOSE_INTL_API
+            R += ToString(callContentFunction(nextElement.toLocaleString, nextElement, locales, options));
+#else
+            R += ToString(callContentFunction(nextElement.toLocaleString, nextElement));
+#endif
+        }
+    }
+
+    // Step 10.
+    return R;
+}
+
 // ES 2016 draft Mar 25, 2016 22.1.2.5.
 function ArraySpecies() {
     // Step 1.
     return this;
 }
 
 // ES 2016 draft Mar 25, 2016 9.4.2.3.
 function ArraySpeciesCreate(originalArray, length) {
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -1123,67 +1123,60 @@ struct ArrayJoinDenseKernelFunctor {
     {}
 
     template <JSValueType Type>
     DenseElementResult operator()() {
         return ArrayJoinDenseKernel<SeparatorOp, Type>(cx, sepOp, obj, length, sb, numProcessed);
     }
 };
 
-template <bool Locale, typename SeparatorOp>
+template <typename SeparatorOp>
 static bool
 ArrayJoinKernel(JSContext* cx, SeparatorOp sepOp, HandleObject obj, uint32_t length,
                StringBuffer& sb)
 {
     uint32_t i = 0;
 
-    if (!Locale && !ObjectMayHaveExtraIndexedProperties(obj)) {
+    if (!ObjectMayHaveExtraIndexedProperties(obj)) {
         ArrayJoinDenseKernelFunctor<SeparatorOp> functor(cx, sepOp, obj, length, sb, &i);
         DenseElementResult result = CallBoxedOrUnboxedSpecialization(functor, obj);
         if (result == DenseElementResult::Failure)
             return false;
     }
 
     if (i != length) {
         RootedValue v(cx);
         while (i < length) {
             if (!CheckForInterrupt(cx))
                 return false;
 
             bool hole;
             if (!GetElement(cx, obj, i, &hole, &v))
                 return false;
             if (!hole && !v.isNullOrUndefined()) {
-                if (Locale) {
-                    RootedValue fun(cx);
-                    if (!GetProperty(cx, v, cx->names().toLocaleString, &fun))
-                        return false;
-
-                    if (!Call(cx, fun, v, &v))
-                        return false;
-                }
                 if (!ValueToStringBuffer(cx, v, sb))
                     return false;
             }
 
             if (++i != length && !sepOp(cx, sb))
                 return false;
         }
     }
 
     return true;
 }
 
-template <bool Locale>
+/* ES5 15.4.4.5 */
 bool
-ArrayJoin(JSContext* cx, CallArgs& args)
+js::array_join(JSContext* cx, unsigned argc, Value* vp)
 {
-    // This method is shared by Array.prototype.join and
-    // Array.prototype.toLocaleString. The steps in ES5 are nearly the same, so
-    // the annotations in this function apply to both toLocaleString and join.
+    JS_CHECK_RECURSION(cx, return false);
+
+    AutoSPSEntry pseudoFrame(cx->runtime(), "Array.prototype.join");
+    CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1
     RootedObject obj(cx, ToObject(cx, args.thisv()));
     if (!obj)
         return false;
 
     AutoCycleDetector detector(cx, obj);
     if (!detector.init())
@@ -1196,33 +1189,33 @@ ArrayJoin(JSContext* cx, CallArgs& args)
 
     // Steps 2 and 3
     uint32_t length;
     if (!GetLengthProperty(cx, obj, &length))
         return false;
 
     // Steps 4 and 5
     RootedLinearString sepstr(cx);
-    if (!Locale && args.hasDefined(0)) {
+    if (args.hasDefined(0)) {
         JSString *s = ToString<CanGC>(cx, args[0]);
         if (!s)
             return false;
         sepstr = s->ensureLinear(cx);
         if (!sepstr)
             return false;
     } else {
         sepstr = cx->names().comma;
     }
 
     // Step 6 is implicit in the loops below.
 
     // An optimized version of a special case of steps 7-11: when length==1 and
     // the 0th element is a string, ToString() of that element is a no-op and
     // so it can be immediately returned as the result.
-    if (length == 1 && !Locale && GetAnyBoxedOrUnboxedInitializedLength(obj) == 1) {
+    if (length == 1 && GetAnyBoxedOrUnboxedInitializedLength(obj) == 1) {
         Value elem0 = GetAnyBoxedOrUnboxedDenseElement(obj, 0);
         if (elem0.isString()) {
             args.rval().set(elem0);
             return true;
         }
     }
 
     StringBuffer sb(cx);
@@ -1239,63 +1232,87 @@ ArrayJoin(JSContext* cx, CallArgs& args)
     }
 
     if (length > 0 && !sb.reserve(res.value()))
         return false;
 
     // Various optimized versions of steps 7-10.
     if (seplen == 0) {
         EmptySeparatorOp op;
-        if (!ArrayJoinKernel<Locale>(cx, op, obj, length, sb))
+        if (!ArrayJoinKernel(cx, op, obj, length, sb))
             return false;
     } else if (seplen == 1) {
         char16_t c = sepstr->latin1OrTwoByteChar(0);
         if (c <= JSString::MAX_LATIN1_CHAR) {
             CharSeparatorOp<Latin1Char> op(c);
-            if (!ArrayJoinKernel<Locale>(cx, op, obj, length, sb))
+            if (!ArrayJoinKernel(cx, op, obj, length, sb))
                 return false;
         } else {
             CharSeparatorOp<char16_t> op(c);
-            if (!ArrayJoinKernel<Locale>(cx, op, obj, length, sb))
+            if (!ArrayJoinKernel(cx, op, obj, length, sb))
                 return false;
         }
     } else {
         StringSeparatorOp op(sepstr);
-        if (!ArrayJoinKernel<Locale>(cx, op, obj, length, sb))
+        if (!ArrayJoinKernel(cx, op, obj, length, sb))
             return false;
     }
 
     // Step 11
     JSString *str = sb.finishString();
     if (!str)
         return false;
 
     args.rval().setString(str);
     return true;
 }
 
-/* ES5 15.4.4.3 */
+// ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f
+// 22.1.3.27 Array.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ])
+// ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276
+// 13.4.1 Array.prototype.toLocaleString ([ locales [ , options ]])
 static bool
 array_toLocaleString(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_CHECK_RECURSION(cx, return false);
 
     CallArgs args = CallArgsFromVp(argc, vp);
-    return ArrayJoin<true>(cx, args);
-}
-
-/* ES5 15.4.4.5 */
-bool
-js::array_join(JSContext* cx, unsigned argc, Value* vp)
-{
-    JS_CHECK_RECURSION(cx, return false);
-
-    AutoSPSEntry pseudoFrame(cx->runtime(), "Array.prototype.join");
-    CallArgs args = CallArgsFromVp(argc, vp);
-    return ArrayJoin<false>(cx, args);
+
+    // Step 1
+    RootedObject obj(cx, ToObject(cx, args.thisv()));
+    if (!obj)
+        return false;
+
+    // Avoid calling into self-hosted code if the array is empty.
+    if (obj->is<ArrayObject>() && obj->as<ArrayObject>().length() == 0) {
+        args.rval().setString(cx->names().empty);
+        return true;
+    }
+    if (obj->is<UnboxedArrayObject>() && obj->as<UnboxedArrayObject>().length() == 0) {
+        args.rval().setString(cx->names().empty);
+        return true;
+    }
+
+    AutoCycleDetector detector(cx, obj);
+    if (!detector.init())
+        return false;
+
+    if (detector.foundCycle()) {
+        args.rval().setString(cx->names().empty);
+        return true;
+    }
+
+    FixedInvokeArgs<2> args2(cx);
+
+    args2[0].set(args.get(0));
+    args2[1].set(args.get(1));
+
+    // Steps 2-10.
+    RootedValue thisv(cx, ObjectValue(*obj));
+    return CallSelfHostedFunction(cx, cx->names().ArrayToLocaleString, thisv, args2, args.rval());
 }
 
 /* vector must point to rooted memory. */
 static bool
 InitArrayElements(JSContext* cx, HandleObject obj, uint32_t start,
                   uint32_t count, const Value* vector,
                   ShouldUpdateTypes updateTypes = ShouldUpdateTypes::Update)
 {
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/Array/toLocaleString-date.js
@@ -0,0 +1,53 @@
+if (typeof Intl === "object") {
+    const localeSep = [,,].toLocaleString();
+
+    const date = new Date(Date.UTC(2012, 11, 12, 3, 0, 0));
+
+    assertEq([date].toLocaleString("en-us", {timeZone: "UTC"}), "12/12/2012, 3:00:00 AM");
+    assertEq([date].toLocaleString(["de", "en"], {timeZone: "UTC"}), "12.12.2012, 03:00:00");
+    assertEq([date].toLocaleString("th-th", {timeZone: "UTC"}), "12/12/2555 03:00:00");
+    assertEq([date].toLocaleString("th-th-u-nu-thai", {timeZone: "UTC"}), "๑๒/๑๒/๒๕๕๕ ๐๓:๐๐:๐๐");
+
+    const sampleValues = [
+        date, new Date(0),
+    ];
+    const sampleLocales = [
+        void 0,
+        "en",
+        "th-th-u-nu-thai",
+        "ja-jp",
+        "ar-ma-u-ca-islamicc",
+        ["tlh", "de"],
+    ];
+    const numericFormatOptions = {
+        timeZone: "UTC",
+        year: "numeric", month: "numeric", day: "numeric",
+        hour: "numeric", minute: "numeric", second: "numeric",
+    };
+    const longFormatOptions = {
+        timeZone: "UTC",
+        year: "numeric", month: "long", day: "numeric",
+        hour: "numeric", minute: "numeric", second: "numeric"
+    };
+    const sampleOptions = [
+        {timeZone: "UTC"},
+        longFormatOptions,
+    ];
+
+    for (let locale of sampleLocales) {
+        for (let options of sampleOptions) {
+            let dtfOptions;
+            if (options === longFormatOptions) {
+                dtfOptions = longFormatOptions;
+            } else {
+                dtfOptions = numericFormatOptions;
+            }
+            let dtf = new Intl.DateTimeFormat(locale, dtfOptions);
+            let expected = sampleValues.map(dtf.format).join(localeSep);
+            assertEq(sampleValues.toLocaleString(locale, options), expected);
+        }
+    }
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/Array/toLocaleString-number.js
@@ -0,0 +1,34 @@
+if (typeof Intl === "object") {
+    const localeSep = [,,].toLocaleString();
+
+    assertEq([NaN].toLocaleString("ar"), "ليس رقم");
+    assertEq([NaN].toLocaleString(["zh-hant", "ar"]), "非數值");
+    assertEq([Infinity].toLocaleString("dz"), "གྲངས་མེད");
+    assertEq([-Infinity].toLocaleString(["fr", "en"]), "-∞");
+
+    const sampleValues = [
+        -0, +0, -1, +1, -2, +2, -0.5, +0.5,
+    ];
+    const sampleLocales = [
+        void 0,
+        "en",
+        "th-th-u-nu-thai",
+        ["tlh", "de"],
+    ];
+    const sampleOptions = [
+        void 0,
+        {},
+        {style: "percent"},
+        {style: "currency", currency: "USD", minimumIntegerDigits: 4},
+    ];
+    for (let locale of sampleLocales) {
+        for (let options of sampleOptions) {
+            let nf = new Intl.NumberFormat(locale, options);
+            let expected = sampleValues.map(nf.format).join(localeSep);
+            assertEq(sampleValues.toLocaleString(locale, options), expected);
+        }
+    }
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/Intl/Array/toLocaleString.js
@@ -0,0 +1,35 @@
+if (typeof Intl === "object") {
+    const localeSep = [,,].toLocaleString();
+
+    // Missing arguments are passed as |undefined|.
+    const objNoArgs = {
+        toLocaleString() {
+            assertEq(arguments.length, 2);
+            assertEq(arguments[0], undefined);
+            assertEq(arguments[1], undefined);
+            return "pass";
+        }
+    };
+    // - Single element case.
+    assertEq([objNoArgs].toLocaleString(), "pass");
+    // - More than one element.
+    assertEq([objNoArgs, objNoArgs].toLocaleString(), "pass" + localeSep + "pass");
+
+    // Ensure "locales" and "options" arguments are passed to the array elements.
+    const locales = {}, options = {};
+    const objWithArgs = {
+        toLocaleString() {
+            assertEq(arguments.length, 2);
+            assertEq(arguments[0], locales);
+            assertEq(arguments[1], options);
+            return "pass";
+        }
+    };
+    // - Single element case.
+    assertEq([objWithArgs].toLocaleString(locales, options), "pass");
+    // - More than one element.
+    assertEq([objWithArgs, objWithArgs].toLocaleString(locales, options), "pass" + localeSep + "pass");
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/toLocaleString-nointl.js
@@ -0,0 +1,26 @@
+if (typeof Intl !== "object") {
+    const localeSep = [,,].toLocaleString();
+
+    const obj = {
+        toLocaleString() {
+            assertEq(arguments.length, 0);
+            return "pass";
+        }
+    };
+
+    // Ensure no arguments are passed to the array elements.
+    // - Single element case.
+    assertEq([obj].toLocaleString(), "pass");
+    // - More than one element.
+    assertEq([obj, obj].toLocaleString(), "pass" + localeSep + "pass");
+
+    // Ensure no arguments are passed to the array elements even if supplied.
+    const locales = {}, options = {};
+    // - Single element case.
+    assertEq([obj].toLocaleString(locales, options), "pass");
+    // - More than one element.
+    assertEq([obj, obj].toLocaleString(locales, options), "pass" + localeSep + "pass");
+}
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -18,16 +18,17 @@
     macro(Any, Any, "Any") \
     macro(apply, apply, "apply") \
     macro(arguments, arguments, "arguments") \
     macro(ArrayBufferSpecies, ArrayBufferSpecies, "ArrayBufferSpecies") \
     macro(ArrayIterator, ArrayIterator, "Array Iterator") \
     macro(ArrayIteratorNext, ArrayIteratorNext, "ArrayIteratorNext") \
     macro(ArraySpecies, ArraySpecies, "ArraySpecies") \
     macro(ArraySpeciesCreate, ArraySpeciesCreate, "ArraySpeciesCreate") \
+    macro(ArrayToLocaleString, ArrayToLocaleString, "ArrayToLocaleString") \
     macro(ArrayType, ArrayType, "ArrayType") \
     macro(ArrayValues, ArrayValues, "ArrayValues") \
     macro(ArrayValuesAt, ArrayValuesAt, "ArrayValuesAt") \
     macro(as, as, "as") \
     macro(Async, Async, "Async") \
     macro(Bool8x16, Bool8x16, "Bool8x16") \
     macro(Bool16x8, Bool16x8, "Bool16x8") \
     macro(Bool32x4, Bool32x4, "Bool32x4") \