Bug 866849, part 2 - Implement Array.of. r=evilpie.
authorJason Orendorff <jorendorff@mozilla.com>
Wed, 10 Jul 2013 08:14:02 -0500
changeset 137970 d0c3168c3c47b8d553da124131a10748bb5df349
parent 137969 98dccff45810620b59a105feeb7a9df048b5b73d
child 137971 c8a1289735aa5dcb93ec4730be01d007b37b6fc8
push id24939
push userryanvm@gmail.com
push dateWed, 10 Jul 2013 17:49:39 +0000
treeherdermozilla-central@dde4dcd6fa46 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersevilpie
bugs866849
milestone25.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 866849, part 2 - Implement Array.of. r=evilpie.
js/src/jit-test/tests/collections/Array-of-1.js
js/src/jit-test/tests/collections/Array-of-2.js
js/src/jit-test/tests/collections/Array-of-3.js
js/src/jit-test/tests/collections/Array-of-4.js
js/src/jit-test/tests/collections/Array-of-cross-compartment.js
js/src/jit-test/tests/collections/Array-of-generic-1.js
js/src/jit-test/tests/collections/Array-of-generic-2.js
js/src/jit-test/tests/collections/Array-of-generic-3.js
js/src/jit-test/tests/collections/Array-of-length-setter-2.js
js/src/jit-test/tests/collections/Array-of-length-setter.js
js/src/jit-test/tests/collections/Array-of-ordering.js
js/src/jit-test/tests/collections/Array-of-surfaces.js
js/src/jsarray.cpp
js/src/jsfun.cpp
js/src/jsfun.h
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-1.js
@@ -0,0 +1,15 @@
+// Array.of makes real arrays.
+
+function check(a) {
+    assertEq(Object.getPrototypeOf(a), Array.prototype);
+    assertEq(Array.isArray(a), true);
+    a[9] = 9;
+    assertEq(a.length, 10);
+}
+
+check(Array.of());
+check(Array.of(0));
+check(Array.of(0, 1, 2));
+
+var f = Array.of;
+check(f());
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-2.js
@@ -0,0 +1,14 @@
+// Array.of basics
+
+load(libdir + "asserts.js");
+
+var a = Array.of();
+assertEq(a.length, 0);
+
+a = Array.of(undefined, null, 3.14, []);
+assertDeepEq(a, [undefined, null, 3.14, []]);
+
+a = [];
+for (var i = 0; i < 1000; i++)
+    a[i] = i;
+assertDeepEq(Array.of.apply({}, a), a);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-3.js
@@ -0,0 +1,8 @@
+// Array.of does not leave holes
+
+load(libdir + "asserts.js");
+
+assertDeepEq(Array.of(undefined), [undefined]);
+assertDeepEq(Array.of(undefined, undefined), [undefined, undefined]);
+assertDeepEq(Array.of.apply(this, [,,undefined]), [undefined, undefined, undefined]);
+assertDeepEq(Array.of.apply(this, Array(4)), [undefined, undefined, undefined, undefined]);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-4.js
@@ -0,0 +1,13 @@
+// Array.of does not trigger prototype setters.
+// (It defines elements rather than assigning to them.)
+
+var status = "pass";
+Object.defineProperty(Array.prototype, "0", {set: v => status = "FAIL 1"});
+assertEq(Array.of(1)[0], 1);
+assertEq(status, "pass");
+
+function Bag() {}
+Bag.of = Array.of;
+Object.defineProperty(Bag.prototype, "0", {set: v => status = "FAIL 2"});
+assertEq(Bag.of(1)[0], 1);
+assertEq(status, "pass");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-cross-compartment.js
@@ -0,0 +1,10 @@
+// Array.of returns an object in the target compartment, not the caller's compartment.
+// This rules out implementations along the lines of (...args) => args.
+
+var g = newGlobal();
+var ga = g.Array.of(1, 2, 3);
+assertEq(ga instanceof g.Array, true);
+
+g.Array.of = Array.of;
+var a = g.Array.of(1, 2, 3); // this-value is a wrapper of g.Array, which IsConstructor, so we call it
+assertEq(ga instanceof g.Array, true);  // it produces a g.Array instance
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-generic-1.js
@@ -0,0 +1,25 @@
+// Array.of can be transplanted to other classes.
+
+load(libdir + "asserts.js");
+
+var hits = 0;
+function Bag() {
+    hits++;
+}
+Bag.of = Array.of;
+
+hits = 0;
+var actual = Bag.of("zero", "one");
+assertEq(hits, 1);
+
+var expected = new Bag;
+expected[0] = "zero";
+expected[1] = "one";
+expected.length = 2;
+assertDeepEq(actual, expected);
+
+hits = 0;
+actual = Array.of.call(Bag, "zero", "one");
+assertEq(hits, 1);
+assertDeepEq(actual, expected);
+
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-generic-2.js
@@ -0,0 +1,11 @@
+// Array.of passes the number of arguments to the constructor it calls.
+
+var hits = 0;
+function Herd(n) {
+    assertEq(arguments.length, 1);
+    assertEq(n, 5);
+    hits++;
+}
+Herd.of = Array.of;
+Herd.of("sheep", "cattle", "elephants", "whales", "seals");
+assertEq(hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-generic-3.js
@@ -0,0 +1,6 @@
+// Array.of can be transplanted to builtin constructors.
+
+load(libdir + "asserts.js");
+
+Uint8Array.of = Array.of;
+assertDeepEq(Uint8Array.of(0x12, 0x34, 0x5678, 0x9abcdef), Uint8Array([0x12, 0x34, 0x78, 0xef]));
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-length-setter-2.js
@@ -0,0 +1,13 @@
+// Array.of does a strict assignment to the new object's .length.
+// The assignment is strict even if the code we're calling from is not strict.
+
+load(libdir + "asserts.js");
+
+function Empty() {}
+Empty.of = Array.of;
+Object.defineProperty(Empty.prototype, "length", {get: () => 0});
+
+var nothing = new Empty;
+nothing.length = 2;  // no exception; this is not a strict mode assignment
+
+assertThrowsInstanceOf(() => Empty.of(), TypeError);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-length-setter.js
@@ -0,0 +1,26 @@
+// Array.of calls a "length" setter if one is present.
+
+var hits = 0;
+var lastObj = null, lastVal = undefined;
+function setter(v) {
+    hits++;
+    lastObj = this;
+    lastVal = v;
+}
+
+// when the setter is on the new object
+function Pack() {
+    Object.defineProperty(this, "length", {set: setter});
+}
+Pack.of = Array.of;
+var pack = Pack.of("wolves", "cards", "cigarettes", "lies");
+assertEq(lastObj, pack);
+assertEq(lastVal, 4);
+
+// when the setter is on the new object's prototype
+function Bevy() {}
+Object.defineProperty(Bevy.prototype, "length", {set: setter});
+Bevy.of = Array.of;
+var bevy = Bevy.of("quail");
+assertEq(lastObj, bevy);
+assertEq(lastVal, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-ordering.js
@@ -0,0 +1,30 @@
+// Order of Array.of operations.
+
+load(libdir + "asserts.js");
+
+var log;
+
+var dstdata = [];
+var dst = new Proxy(dstdata, {
+    defineProperty: function (t, name, desc) {
+        log.push(["def", name, desc.value]);
+    },
+    set: function (t, name, value) {
+        log.push(["set", name, value]);
+    }
+});
+
+function Troop() {
+    return dst;
+}
+Troop.of = Array.of;
+
+log = [];
+assertEq(Troop.of("monkeys", "baboons", "kangaroos"), dst);
+assertDeepEq(log, [
+    ["def", "0", "monkeys"],
+    ["def", "1", "baboons"],
+    ["def", "2", "kangaroos"],
+    ["set", "length", 3]
+]);
+
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Array-of-surfaces.js
@@ -0,0 +1,14 @@
+// Check superficial features of Array.of.
+
+load(libdir + "asserts.js");
+
+var desc = Object.getOwnPropertyDescriptor(Array, "of");
+assertEq(desc.configurable, true);
+assertEq(desc.enumerable, false);
+assertEq(desc.writable, true);
+assertEq(Array.of.length, 0);
+assertThrowsInstanceOf(() => new Array.of(), TypeError);  // not a constructor
+
+// When the this-value passed in is not a constructor, the result is an array.
+for (let v of [undefined, null, false, "cow"])
+    assertEq(Array.isArray(Array.of.call(v)), true);
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -2691,16 +2691,85 @@ static JSBool
 array_isArray(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     bool isArray = args.length() > 0 && IsObjectWithClass(args[0], ESClass_Array, cx);
     args.rval().setBoolean(isArray);
     return true;
 }
 
+static bool
+IsArrayConstructor(const Value &v)
+{
+    // This must only return true if v is *the* Array constructor for the
+    // current compartment; we rely on the fact that any other Array
+    // constructor would be represented as a wrapper.
+    return v.isObject() &&
+           v.toObject().is<JSFunction>() &&
+           v.toObject().as<JSFunction>().isNative() &&
+           v.toObject().as<JSFunction>().native() == js_Array;
+}
+
+static JSBool
+ArrayFromCallArgs(JSContext *cx, RootedTypeObject &type, CallArgs &args)
+{
+    if (!InitArrayTypes(cx, type, args.array(), args.length()))
+        return false;
+    JSObject *obj = (args.length() == 0)
+        ? NewDenseEmptyArray(cx)
+        : NewDenseCopiedArray(cx, args.length(), args.array());
+    if (!obj)
+        return false;
+    obj->setType(type);
+    args.rval().setObject(*obj);
+    return true;
+}
+
+static JSBool
+array_of(JSContext *cx, unsigned argc, Value *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (IsArrayConstructor(args.thisv()) || !IsConstructor(args.thisv())) {
+        // IsArrayConstructor(this) will usually be true in practice. This is
+        // the most common path.
+        RootedTypeObject type(cx, GetTypeCallerInitObject(cx, JSProto_Array));
+        if (!type)
+            return false;
+        return ArrayFromCallArgs(cx, type, args);
+    }
+
+    // Step 4.
+    RootedObject obj(cx);
+    {
+        RootedValue v(cx);
+        Value argv[1] = {NumberValue(argc)};
+        if (!InvokeConstructor(cx, args.thisv(), 1, argv, v.address()))
+            return false;
+        obj = ToObject(cx, v);
+        if (!obj)
+            return false;
+    }
+
+    // Step 8.
+    for (unsigned k = 0; k < argc; k++) {
+        if (!JSObject::defineElement(cx, obj, k, args.handleAt(k)))
+            return false;
+    }
+
+    // Steps 9-10.
+    RootedValue v(cx, NumberValue(argc));
+    if (!JSObject::setProperty(cx, obj, obj, cx->names().length, &v, true))
+        return false;
+
+    // Step 11.
+    args.rval().setObject(*obj);
+    return true;
+}
+
 #define GENERIC JSFUN_GENERIC_NATIVE
 
 static const JSFunctionSpec array_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,      array_toSource,     0,0),
 #endif
     JS_FN(js_toString_str,      array_toString,     0,0),
     JS_FN(js_toLocaleString_str,array_toLocaleString,0,0),
@@ -2741,40 +2810,31 @@ static const JSFunctionSpec array_static
          {"lastIndexOf",        {NULL, NULL},       2,0, "ArrayStaticLastIndexOf"},
          {"indexOf",            {NULL, NULL},       2,0, "ArrayStaticIndexOf"},
          {"forEach",            {NULL, NULL},       2,0, "ArrayStaticForEach"},
          {"map",                {NULL, NULL},       2,0, "ArrayStaticMap"},
          {"every",              {NULL, NULL},       2,0, "ArrayStaticEvery"},
          {"some",               {NULL, NULL},       2,0, "ArrayStaticSome"},
          {"reduce",             {NULL, NULL},       2,0, "ArrayStaticReduce"},
          {"reduceRight",        {NULL, NULL},       2,0, "ArrayStaticReduceRight"},
+    JS_FN("of",                 array_of,           0,0),
     JS_FS_END
 };
 
 /* ES5 15.4.2 */
 JSBool
 js_Array(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     RootedTypeObject type(cx, GetTypeCallerInitObject(cx, JSProto_Array));
     if (!type)
         return false;
 
-    if (args.length() != 1 || !args[0].isNumber()) {
-        if (!InitArrayTypes(cx, type, args.array(), args.length()))
-            return false;
-        JSObject *obj = (args.length() == 0)
-                        ? NewDenseEmptyArray(cx)
-                        : NewDenseCopiedArray(cx, args.length(), args.array());
-        if (!obj)
-            return false;
-        obj->setType(type);
-        args.rval().setObject(*obj);
-        return true;
-    }
+    if (args.length() != 1 || !args[0].isNumber())
+        return ArrayFromCallArgs(cx, type, args);
 
     uint32_t length;
     if (args[0].isInt32()) {
         int32_t i = args[0].toInt32();
         if (i < 0) {
             JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BAD_ARRAY_LENGTH);
             return false;
         }
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -1681,16 +1681,33 @@ js::DefineFunction(JSContext *cx, Handle
 
     RootedValue funVal(cx, ObjectValue(*fun));
     if (!JSObject::defineGeneric(cx, obj, id, funVal, gop, sop, flags & ~JSFUN_FLAGS_MASK))
         return NULL;
 
     return fun;
 }
 
+bool
+js::IsConstructor(const Value &v)
+{
+    // Step 2.
+    if (!v.isObject())
+        return false;
+
+    // Step 3-4, a bit complex for us, since we have several flavors of
+    // [[Construct]] internal method.
+    JSObject &obj = v.toObject();
+    if (obj.is<JSFunction>()) {
+        JSFunction &fun = obj.as<JSFunction>();
+        return fun.isNativeConstructor() || fun.isInterpretedConstructor();
+    }
+    return obj.getClass()->construct != NULL;
+}
+
 void
 js::ReportIncompatibleMethod(JSContext *cx, CallReceiver call, Class *clasp)
 {
     RootedValue thisv(cx, call.thisv());
 
 #ifdef DEBUG
     if (thisv.isObject()) {
         JS_ASSERT(thisv.toObject().getClass() != clasp ||
--- a/js/src/jsfun.h
+++ b/js/src/jsfun.h
@@ -392,16 +392,19 @@ NewFunction(JSContext *cx, HandleObject 
             NewObjectKind newKind = GenericObject);
 
 extern JSFunction *
 DefineFunction(JSContext *cx, HandleObject obj, HandleId id, JSNative native,
                unsigned nargs, unsigned flags,
                gc::AllocKind allocKind = JSFunction::FinalizeKind,
                NewObjectKind newKind = GenericObject);
 
+// ES6 9.2.5 IsConstructor
+bool IsConstructor(const Value &v);
+
 /*
  * Function extended with reserved slots for use by various kinds of functions.
  * Most functions do not have these extensions, but enough do that efficient
  * storage is required (no malloc'ed reserved slots).
  */
 class FunctionExtended : public JSFunction
 {
   public: