Bug 1364854 - Port Object.assign to C++. r=evilpie
authorJan de Mooij <jdemooij@mozilla.com>
Tue, 27 Jun 2017 11:05:15 -0700
changeset 366269 f165f830468d42546e03a1770286db0ef561ff1e
parent 366268 4579ac0924090ade37b93ebe3b55bbab0b7859c8
child 366270 9db369ba372ccc31274de3ecd6598e6a16ca1995
push id32099
push usercbook@mozilla.com
push dateWed, 28 Jun 2017 11:23:49 +0000
treeherdermozilla-central@306d2070e105 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersevilpie
bugs1364854
milestone56.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 1364854 - Port Object.assign to C++. r=evilpie
js/src/builtin/Object.cpp
js/src/builtin/Object.js
js/src/jit-test/tests/basic/object-assign.js
--- a/js/src/builtin/Object.cpp
+++ b/js/src/builtin/Object.cpp
@@ -588,16 +588,197 @@ obj_setPrototypeOf(JSContext* cx, unsign
     if (!SetPrototype(cx, obj, newProto))
         return false;
 
     /* Step 8. */
     args.rval().set(args[0]);
     return true;
 }
 
+static bool
+PropertyIsEnumerable(JSContext* cx, HandleObject obj, HandleId id, bool* enumerable)
+{
+    PropertyResult prop;
+    if (obj->isNative() &&
+        NativeLookupOwnProperty<NoGC>(cx, &obj->as<NativeObject>(), id, &prop))
+    {
+        if (!prop) {
+            *enumerable = false;
+            return true;
+        }
+
+        unsigned attrs = GetPropertyAttributes(obj, prop);
+        *enumerable = (attrs & JSPROP_ENUMERATE) != 0;
+        return true;
+    }
+
+    Rooted<PropertyDescriptor> desc(cx);
+    if (!GetOwnPropertyDescriptor(cx, obj, id, &desc))
+        return false;
+
+    *enumerable = desc.object() && desc.enumerable();
+    return true;
+}
+
+static bool
+TryAssignNative(JSContext* cx, HandleObject to, HandleObject from, bool* optimized)
+{
+    *optimized = false;
+
+    if (!from->isNative() || !to->isNative())
+        return true;
+
+    // Don't use the fast path if |from| may have extra indexed or lazy
+    // properties.
+    NativeObject* fromNative = &from->as<NativeObject>();
+    if (fromNative->getDenseInitializedLength() > 0 ||
+        fromNative->isIndexed() ||
+        fromNative->is<TypedArrayObject>() ||
+        fromNative->getClass()->getNewEnumerate() ||
+        fromNative->getClass()->getEnumerate())
+    {
+        return true;
+    }
+
+    // Get a list of |from| shapes. As long as from->lastProperty() == fromShape
+    // we can use this to speed up both the enumerability check and the GetProp.
+
+    using ShapeVector = GCVector<Shape*, 8>;
+    Rooted<ShapeVector> shapes(cx, ShapeVector(cx));
+
+    RootedShape fromShape(cx, fromNative->lastProperty());
+    for (Shape::Range<NoGC> r(fromShape); !r.empty(); r.popFront()) {
+        // Symbol properties need to be assigned last. For now fall back to the
+        // slow path if we see a symbol property.
+        if (MOZ_UNLIKELY(JSID_IS_SYMBOL(r.front().propidRaw())))
+            return true;
+        if (MOZ_UNLIKELY(!shapes.append(&r.front())))
+            return false;
+    }
+
+    *optimized = true;
+
+    RootedShape shape(cx);
+    RootedValue propValue(cx);
+    RootedId nextKey(cx);
+    RootedValue toReceiver(cx, ObjectValue(*to));
+
+    for (size_t i = shapes.length(); i > 0; i--) {
+        shape = shapes[i - 1];
+        nextKey = shape->propid();
+
+        // Ensure |from| is still native: a getter/setter might have turned
+        // |from| or |to| into an unboxed object or it could have been swapped
+        // with a non-native object.
+        if (MOZ_LIKELY(from->isNative() &&
+                       from->as<NativeObject>().lastProperty() == fromShape &&
+                       shape->hasDefaultGetter() &&
+                       shape->hasSlot()))
+        {
+            if (!shape->enumerable())
+                continue;
+            propValue = from->as<NativeObject>().getSlot(shape->slot());
+        } else {
+            // |from| changed shape or the property is not a data property, so
+            // we have to do the slower enumerability check and GetProp.
+            bool enumerable;
+            if (!PropertyIsEnumerable(cx, from, nextKey, &enumerable))
+                return false;
+            if (!enumerable)
+                continue;
+            if (!GetProperty(cx, from, from, nextKey, &propValue))
+                return false;
+        }
+
+        ObjectOpResult result;
+        if (MOZ_UNLIKELY(!SetProperty(cx, to, nextKey, propValue, toReceiver, result)))
+            return false;
+        if (MOZ_UNLIKELY(!result.checkStrict(cx, to, nextKey)))
+            return false;
+    }
+
+    return true;
+}
+
+static bool
+AssignSlow(JSContext* cx, HandleObject to, HandleObject from)
+{
+    // Step 4.b.ii.
+    AutoIdVector keys(cx);
+    if (!GetPropertyKeys(cx, from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys))
+        return false;
+
+    // Step 4.c.
+    RootedId nextKey(cx);
+    RootedValue propValue(cx);
+    for (size_t i = 0, len = keys.length(); i < len; i++) {
+        nextKey = keys[i];
+
+        // Step 4.c.i.
+        bool enumerable;
+        if (MOZ_UNLIKELY(!PropertyIsEnumerable(cx, from, nextKey, &enumerable)))
+            return false;
+        if (!enumerable)
+            continue;
+
+        // Step 4.c.ii.1.
+        if (MOZ_UNLIKELY(!GetProperty(cx, from, from, nextKey, &propValue)))
+            return false;
+
+        // Step 4.c.ii.2.
+        if (MOZ_UNLIKELY(!SetProperty(cx, to, nextKey, propValue)))
+            return false;
+    }
+
+    return true;
+}
+
+// ES2018 draft rev 48ad2688d8f964da3ea8c11163ef20eb126fb8a4
+// 19.1.2.1 Object.assign(target, ...sources)
+static bool
+obj_assign(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1.
+    RootedObject to(cx, ToObject(cx, args.get(0)));
+    if (!to)
+        return false;
+
+    // Note: step 2 is implicit. If there are 0 arguments, ToObject throws. If
+    // there's 1 argument, the loop below is a no-op.
+
+    // Step 4.
+    RootedObject from(cx);
+    for (size_t i = 1; i < args.length(); i++) {
+        // Step 4.a.
+        if (args[i].isNullOrUndefined())
+            continue;
+
+        // Step 4.b.i.
+        from = ToObject(cx, args[i]);
+        if (!from)
+            return false;
+
+        // Steps 4.b.ii, 4.c.
+        bool optimized;
+        if (!TryAssignNative(cx, to, from, &optimized))
+            return false;
+        if (optimized)
+            continue;
+
+        if (!AssignSlow(cx, to, from))
+            return false;
+    }
+
+    // Step 5.
+    args.rval().setObject(*to);
+    return true;
+}
+
 #if JS_HAS_OBJ_WATCHPOINT
 
 bool
 js::WatchHandler(JSContext* cx, JSObject* obj_, jsid id_, const JS::Value& old,
                  JS::Value* nvp, void* closure)
 {
     RootedObject obj(cx, obj_);
     RootedId id(cx, id_);
@@ -1290,17 +1471,17 @@ static const JSFunctionSpec object_metho
 static const JSPropertySpec object_properties[] = {
 #if JS_HAS_OBJ_PROTO_PROP
     JS_PSGS("__proto__", ProtoGetter, ProtoSetter, 0),
 #endif
     JS_PS_END
 };
 
 static const JSFunctionSpec object_static_methods[] = {
-    JS_SELF_HOSTED_FN("assign",        "ObjectStaticAssign",        2, 0),
+    JS_FN("assign",                    obj_assign,                  2, 0),
     JS_SELF_HOSTED_FN("getPrototypeOf", "ObjectGetPrototypeOf",     1, 0),
     JS_FN("setPrototypeOf",            obj_setPrototypeOf,          2, 0),
     JS_FN("getOwnPropertyDescriptor",  obj_getOwnPropertyDescriptor,2, 0),
     JS_SELF_HOSTED_FN("getOwnPropertyDescriptors", "ObjectGetOwnPropertyDescriptors", 1, 0),
     JS_FN("keys",                      obj_keys,                    1, 0),
     JS_FN("values",                    obj_values,                  1, 0),
     JS_FN("entries",                   obj_entries,                 1, 0),
     JS_FN("is",                        obj_is,                      2, 0),
--- a/js/src/builtin/Object.js
+++ b/js/src/builtin/Object.js
@@ -1,51 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// ES6 draft rev36 2015-03-17 19.1.2.1
-function ObjectStaticAssign(target, firstSource) {
-    // Steps 1-2.
-    var to = ToObject(target);
-
-    // Step 3.
-    if (arguments.length < 2)
-        return to;
-
-    // Steps 4-5.
-    for (var i = 1; i < arguments.length; i++) {
-        // Step 5.a.
-        var nextSource = arguments[i];
-        if (nextSource === null || nextSource === undefined)
-            continue;
-
-        // Steps 5.b.i-ii.
-        var from = ToObject(nextSource);
-
-        // Steps 5.b.iii-iv.
-        var keys = OwnPropertyKeys(from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS);
-
-        // Step 5.c.
-        for (var nextIndex = 0, len = keys.length; nextIndex < len; nextIndex++) {
-            var nextKey = keys[nextIndex];
-
-            // Steps 5.c.i-iii. We abbreviate this by calling propertyIsEnumerable
-            // which is faster and returns false for not defined properties.
-            if (callFunction(std_Object_propertyIsEnumerable, from, nextKey)) {
-                // Steps 5.c.iii.1-4.
-                to[nextKey] = from[nextKey];
-            }
-        }
-    }
-
-    // Step 6.
-    return to;
-}
-
 // ES stage 4 proposal
 function ObjectGetOwnPropertyDescriptors(O) {
     // Step 1.
     var obj = ToObject(O);
 
     // Step 2.
     var keys = OwnPropertyKeys(obj, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS);
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/object-assign.js
@@ -0,0 +1,135 @@
+function test() {
+    var from, to;
+
+    // Property changes value.
+    from = {x: 1, y: 2};
+    to = {set x(v) { from.y = 5; }};
+    Object.assign(to, from);
+    assertEq(to.y, 5);
+
+    // Property becomes a getter.
+    from = {x: 1, y: 2};
+    to = {set x(v) { Object.defineProperty(from, "y", {get: () => 4}); }};
+    Object.assign(to, from);
+    assertEq(to.y, 4);
+
+    // Property becomes non-enumerable.
+    from = {x: 1, y: 2};
+    to = {set x(v) { Object.defineProperty(from, "y", {value: 2,
+						       enumerable: false,
+						       configurable: true,
+						       writable: true}); }};
+    Object.assign(to, from);
+    assertEq("y" in to, false);
+    to = {};
+    Object.assign(to, from);
+    assertEq("y" in to, false);
+
+    // Property is deleted. Should NOT get Object.prototype.toString.
+    from = {x: 1, toString: 2};
+    to = {set x(v) { delete from.toString; }};
+    Object.assign(to, from);
+    assertEq(to.hasOwnProperty("toString"), false);
+
+    from = {toString: 2, x: 1};
+    to = {set x(v) { delete from.toString; }};
+    Object.assign(to, from);
+    assertEq(to.toString, 2);
+
+    from = {x: 1, toString: 2, y: 3};
+    to = {set x(v) { delete from.toString; }};
+    Object.assign(to, from);
+    assertEq(to.hasOwnProperty("toString"), false);
+    assertEq(to.y, 3);
+
+    // New property is added.
+    from = {x: 1, y: 2};
+    to = {set x(v) { from.z = 3; }};
+    Object.assign(to, from);
+    assertEq("z" in to, false);
+
+    // From getter.
+    var c = 7;
+    from = {x: 1, get y() { return ++c; }};
+    to = {};
+    Object.assign(to, from);
+    Object.assign(to, from, from);
+    assertEq(to.y, 10);
+
+    // Frozen object.
+    from = {x: 1, y: 2};
+    to = {x: 4};
+    Object.freeze(to);
+    var ex;
+    try {
+	Object.assign(to, from);
+    } catch (e) {
+	ex = e;
+    }
+    assertEq(ex instanceof TypeError, true);
+    assertEq(to.x, 4);
+
+    // Non-writable property.
+    from = {x: 1, y: 2, z: 3};
+    to = {};
+    Object.defineProperty(to, "y", {value: 9, writable: false});
+    ex = null;
+    try {
+	Object.assign(to, from);
+    } catch(e) {
+	ex = e;
+    }
+    assertEq(ex instanceof TypeError, true);
+    assertEq(to.x, 1);
+    assertEq(to.y, 9);
+    assertEq(to.z, undefined);
+
+    // Array with dense elements.
+    from = [1, 2, 3];
+    to = {};
+    Object.assign(to, from);
+    assertEq(to[2], 3);
+    assertEq("length" in to, false);
+
+    // Object with sparse elements and symbols.
+    from = {x: 1, 1234567: 2,  1234560: 3,[Symbol.iterator]: 5, z: 3};
+    to = {};
+    Object.assign(to, from);
+    assertEq(to[1234567], 2);
+    assertEq(Object.keys(to).toString(), "1234560,1234567,x,z");
+    assertEq(to[Symbol.iterator], 5);
+
+    // Symbol properties need to be assigned last.
+    from = {x: 1, [Symbol.iterator]: 2, y: 3};
+    to = {set y(v) { throw 9; }};
+    ex = null;
+    try {
+	Object.assign(to, from);
+    } catch (e) {
+	ex = e;
+    }
+    assertEq(ex, 9);
+    assertEq(to.x, 1);
+    assertEq(to.hasOwnProperty(Symbol.iterator), false);
+
+    // Typed array.
+    from = new Int32Array([1, 2, 3]);
+    to = {};
+    Object.assign(to, from);
+    assertEq(to[1], 2);
+
+    // Primitive string.
+    from = "foo";
+    to = {};
+    Object.assign(to, from);
+    assertEq(to[0], "f");
+
+    // String object.
+    from = new String("bar");
+    to = {};
+    Object.assign(to, from);
+    assertEq(to[2], "r");
+}
+test();
+test();
+test();