Bug 1073816 - Implement ES6 Function.prototype.bind. r=Till
☠☠ backed out by 9b6aa9b4039b ☠ ☠
authorTom Schuster <evilpies@gmail.com>
Mon, 09 Feb 2015 19:42:20 +0100
changeset 241816 fd9745f7a6970fb07d400899d86bec8f417646e4
parent 241808 9bdf571458b070127fb2aa6c6f23c44dd74165b1
child 241817 41e390a2cf6ee8d82c9c7d94442db3d08cc63886
push id624
push userdburns@mozilla.com
push dateTue, 10 Feb 2015 13:30:25 +0000
reviewersTill
bugs1073816
milestone38.0a1
Bug 1073816 - Implement ES6 Function.prototype.bind. r=Till
js/src/jsfun.cpp
js/src/jsfun.h
js/src/tests/ecma_6/Function/bound-length-and-name.js
js/src/tests/js1_8_5/extensions/scripted-proxies.js
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -499,20 +499,20 @@ js::fun_resolve(JSContext *cx, HandleObj
         //     assertEq(f.length, 0);  // gets Function.prototype.length!
         //     assertEq(f.name, "");  // gets Function.prototype.name!
         // We use the RESOLVED_LENGTH and RESOLVED_NAME flags as a hack to prevent this
         // bug.
         if (isLength) {
             if (fun->hasResolvedLength())
                 return true;
 
-            if (fun->isInterpretedLazy() && !fun->getOrCreateScript(cx))
+            uint16_t length;
+            if (!fun->getLength(cx, &length))
                 return false;
-            uint16_t length = fun->hasScript() ? fun->nonLazyScript()->funLength() :
-                fun->nargs() - fun->hasRest();
+
             v.setInt32(length);
         } else {
             if (fun->hasResolvedName())
                 return true;
 
             v.setString(fun->atom() == nullptr ? cx->runtime()->emptyString : fun->atom());
         }
 
@@ -1621,83 +1621,130 @@ fun_isGenerator(JSContext *cx, unsigned 
         args.rval().setBoolean(false);
         return true;
     }
 
     args.rval().setBoolean(fun->isGenerator());
     return true;
 }
 
-/* ES5 15.3.4.5. */
+// ES6 draft rev32 19.2.3.2
 bool
 js::fun_bind(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    /* Step 1. */
+    // Step 1.
     RootedValue thisv(cx, args.thisv());
 
-    /* Step 2. */
+    // Step 2.
     if (!IsCallable(thisv)) {
         ReportIncompatibleMethod(cx, args, &JSFunction::class_);
         return false;
     }
 
-    /* Step 3. */
+    // Step 3.
     Value *boundArgs = nullptr;
     unsigned argslen = 0;
     if (args.length() > 1) {
         boundArgs = args.array() + 1;
         argslen = args.length() - 1;
     }
 
-    /* Steps 7-9. */
+    // Steps 4-14.
     RootedValue thisArg(cx, args.length() >= 1 ? args[0] : UndefinedValue());
     RootedObject target(cx, &thisv.toObject());
     JSObject *boundFunction = js_fun_bind(cx, target, thisArg, boundArgs, argslen);
     if (!boundFunction)
         return false;
 
-    /* Step 22. */
+    // Step 15.
     args.rval().setObject(*boundFunction);
     return true;
 }
 
 JSObject*
 js_fun_bind(JSContext *cx, HandleObject target, HandleValue thisArg,
             Value *boundArgs, unsigned argslen)
 {
-    /* Steps 15-16. */
-    unsigned length = 0;
-    if (target->is<JSFunction>()) {
-        unsigned nargs = target->as<JSFunction>().nargs();
-        if (nargs > argslen)
-            length = nargs - argslen;
+    double length = 0.0;
+    // Try to avoid invoking the resolve hook.
+    if (target->is<JSFunction>() && !target->as<JSFunction>().hasResolvedLength()) {
+        uint16_t len;
+        if (!target->as<JSFunction>().getLength(cx, &len))
+            return nullptr;
+        length = Max(0.0, double(len) - argslen);
+    } else {
+        // Steps 5-6.
+        RootedId id(cx, NameToId(cx->names().length));
+        bool hasLength;
+        if (!HasOwnProperty(cx, target, id, &hasLength))
+            return nullptr;
+
+        // Step 7-8.
+        if (hasLength) {
+            // a-b.
+            RootedValue targetLen(cx);
+            if (!GetProperty(cx, target, target, id, &targetLen))
+                return nullptr;
+            // d.
+            if (targetLen.isNumber())
+                length = Max(0.0, JS::ToInteger(targetLen.toNumber()) - argslen);
+        }
     }
 
-    /* Step 4-6, 10-11. */
-    RootedAtom name(cx, target->is<JSFunction>() ? target->as<JSFunction>().atom() : nullptr);
+    RootedString name(cx, cx->names().empty);
+    if (target->is<JSFunction>() && !target->as<JSFunction>().hasResolvedName()) {
+        if (target->as<JSFunction>().atom())
+            name = target->as<JSFunction>().atom();
+    } else {
+        // Steps 11-12.
+        RootedValue targetName(cx);
+        if (!GetProperty(cx, target, target, cx->names().name, &targetName))
+            return nullptr;
 
+        // Step 13.
+        if (targetName.isString())
+            name = targetName.toString();
+    }
+
+    // Step 14. Relevant bits from SetFunctionName.
+    StringBuffer sb(cx);
+    if (!sb.append("bound ") || !sb.append(name))
+        return nullptr;
+
+    RootedAtom nameAtom(cx, sb.finishAtom());
+    if (!nameAtom)
+        return nullptr;
+
+    //  Step 4.
     JSFunction::Flags flags = target->isConstructor() ? JSFunction::NATIVE_CTOR
                                                       : JSFunction::NATIVE_FUN;
-    RootedObject funobj(cx, NewFunction(cx, js::NullPtr(), CallOrConstructBoundFunction, length,
-                                        flags, target, name));
-    if (!funobj)
+    RootedFunction fun(cx, NewFunction(cx, js::NullPtr(), CallOrConstructBoundFunction, length,
+                                       flags, target, nameAtom));
+    if (!fun)
+        return nullptr;
+
+    // NB: Bound functions abuse |parent| to store their target.
+    MOZ_ASSERT(fun->getParent() == target);
+
+    if (!fun->initBoundFunction(cx, thisArg, boundArgs, argslen))
         return nullptr;
 
-    /* NB: Bound functions abuse |parent| to store their target. */
-    if (!JSObject::setParent(cx, funobj, target))
-        return nullptr;
+    // Steps 9-10. Set length again, because NewFunction sometimes truncates.
+    if (length != fun->nargs()) {
+        RootedValue lengthVal(cx, NumberValue(length));
+        if (!DefineProperty(cx, fun, cx->names().length, lengthVal, nullptr, nullptr,
+                            JSPROP_READONLY))
+        {
+            return nullptr;
+        }
+    }
 
-    if (!funobj->as<JSFunction>().initBoundFunction(cx, thisArg, boundArgs, argslen))
-        return nullptr;
-
-    /* Steps 17, 19-21 are handled by fun_resolve. */
-    /* Step 18 is the default for new functions. */
-    return funobj;
+    return fun;
 }
 
 /*
  * Report "malformed formal parameter" iff no illegal char or similar scanner
  * error was already reported.
  */
 static bool
 OnBadFormal(JSContext *cx)
--- a/js/src/jsfun.h
+++ b/js/src/jsfun.h
@@ -319,16 +319,26 @@ class JSFunction : public js::NativeObje
     }
 
     JSScript *nonLazyScript() const {
         MOZ_ASSERT(hasScript());
         MOZ_ASSERT(u.i.s.script_);
         return u.i.s.script_;
     }
 
+    bool getLength(JSContext *cx, uint16_t *length) {
+        JS::RootedFunction self(cx, this);
+        if (self->isInterpretedLazy() && !self->getOrCreateScript(cx))
+            return false;
+
+        *length = self->hasScript() ? self->nonLazyScript()->funLength()
+                                    : (self->nargs() - self->hasRest());
+        return true;
+    }
+
     // Returns non-callsited-clone version of this.  Use when return
     // value can flow to arbitrary JS (see Bug 944975).
     JSFunction* originalFunction() {
         if (this->hasScript() && this->nonLazyScript()->isCallsiteClone()) {
             return this->nonLazyScript()->donorFunction();
         } else {
             return this;
         }
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Function/bound-length-and-name.js
@@ -0,0 +1,40 @@
+var proxy = new Proxy(function() {}, {
+    getOwnPropertyDescriptor(target, name) {
+        assertEq(name, "length");
+        return {value: 3, configurable: true};
+    },
+
+    get(target, name) {
+        if (name == "length")
+            return 3;
+        if (name == "name")
+            return "hello world";
+        assertEq(false, true);
+    }
+})
+
+var bound = Function.prototype.bind.call(proxy);
+assertEq(bound.name, "bound hello world");
+assertEq(bound.length, 3);
+
+var fun = function() {};
+Object.defineProperty(fun, "name", {value: 1337});
+Object.defineProperty(fun, "length", {value: "15"});
+bound = fun.bind();
+assertEq(bound.name, "bound ");
+assertEq(bound.length, 0);
+
+Object.defineProperty(fun, "length", {value: Number.MAX_SAFE_INTEGER});
+bound = fun.bind();
+assertEq(bound.length, Number.MAX_SAFE_INTEGER);
+
+Object.defineProperty(fun, "length", {value: -100});
+bound = fun.bind();
+assertEq(bound.length, 0);
+
+fun = function f(a, ...b) { };
+assertEq(fun.length, 1);
+bound = fun.bind();
+assertEq(bound.length, 1);
+
+reportCompare(0, 0, 'ok');
--- a/js/src/tests/js1_8_5/extensions/scripted-proxies.js
+++ b/js/src/tests/js1_8_5/extensions/scripted-proxies.js
@@ -26,37 +26,49 @@ function test() {
     testObj(new Array());
     testObj(new RegExp());
     testObj(Date);
     testObj(Array);
     testObj(RegExp);
 
     /* Test function proxies. */
     var proxy = Proxy.createFunction({
-        get: function(obj,name) { return Function.prototype[name]; },
-	fix: function() {
-	    return ({});
-	}
+        get: function(obj, name) {
+            return Function.prototype[name];
+        },
+        getOwnPropertyDescriptor: function(obj, name) {
+            return Object.getOwnPropertyDescriptor(Function.prototype, name);
+        },
+        fix: function() {
+            return ({});
+        }
     }, function() { return "call"; });
 
     assertEq(proxy(), "call");
     assertEq(Function.prototype.bind.call(proxy)(), "call");
     assertEq(typeof proxy, "function");
     if ("isTrapping" in Proxy) {
 	assertEq(Proxy.isTrapping(proxy), true);
 	assertEq(Proxy.fix(proxy), true);
 	assertEq(Proxy.isTrapping(proxy), false);
 	assertEq(typeof proxy, "function");
 	assertEq(proxy(), "call");
     }
 
     /* Test function proxies as constructors. */
     var proxy = Proxy.createFunction({
-        get: function(obj, name) { return Function.prototype[name]; },
-	fix: function() { return ({}); }
+        get: function(obj, name) {
+            return Function.prototype[name];
+        },
+        getOwnPropertyDescriptor: function(obj, name) {
+            return Object.getOwnPropertyDescriptor(Function.prototype, name);
+        },
+        fix: function() {
+            return ({});
+        }
     },
     function() { var x = {}; x.origin = "call"; return x; },
     function() { var x = {}; x.origin = "new"; return x; })
 
     assertEq(proxy().origin, "call");
     assertEq(Function.prototype.bind.call(proxy)().origin, "call");
     assertEq((new proxy()).origin, "new");
     assertEq(new (Function.prototype.bind.call(proxy))().origin, "new");