From 3b29f85ea8f22ae679c078fca5dffff4d28e1a69 Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Mon, 9 Feb 2015 19:42:20 +0100 Subject: [PATCH] Bug 1073816 - Implement ES6 Function.prototype.bind. r=Till --- js/src/jsfun.cpp | 101 +++++++++++++----- js/src/jsfun.h | 10 ++ .../ecma_6/Function/bound-length-and-name.js | 40 +++++++ .../js1_8_5/extensions/scripted-proxies.js | 24 +++-- 4 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 js/src/tests/ecma_6/Function/bound-length-and-name.js diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 867977a0f5d..bc1b152fb98 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -504,10 +504,10 @@ js::fun_resolve(JSContext *cx, HandleObject obj, HandleId id, bool *resolvedp) 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()) @@ -1626,22 +1626,22 @@ fun_isGenerator(JSContext *cx, unsigned argc, Value *vp) 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) { @@ -1649,14 +1649,14 @@ js::fun_bind(JSContext *cx, unsigned argc, Value *vp) 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; } @@ -1665,34 +1665,81 @@ JSObject* js_fun_bind(JSContext *cx, HandleObject target, HandleValue thisArg, Value *boundArgs, unsigned argslen) { - /* Steps 15-16. */ - unsigned length = 0; - if (target->is()) { - unsigned nargs = target->as().nargs(); - if (nargs > argslen) - length = nargs - argslen; + double length = 0.0; + // Try to avoid invoking the resolve hook. + if (target->is() && !target->as().hasResolvedLength()) { + uint16_t len; + if (!target->as().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() ? target->as().atom() : nullptr); + RootedString name(cx, cx->names().empty); + if (target->is() && !target->as().hasResolvedName()) { + if (target->as().atom()) + name = target->as().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. */ - if (!JSObject::setParent(cx, funobj, target)) + // NB: Bound functions abuse |parent| to store their target. + MOZ_ASSERT(fun->getParent() == target); + + if (!fun->initBoundFunction(cx, thisArg, boundArgs, argslen)) return nullptr; - if (!funobj->as().initBoundFunction(cx, thisArg, boundArgs, argslen)) - 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; + } + } - /* Steps 17, 19-21 are handled by fun_resolve. */ - /* Step 18 is the default for new functions. */ - return funobj; + return fun; } /* diff --git a/js/src/jsfun.h b/js/src/jsfun.h index 3e7b63567c4..41e2ae7264b 100644 --- a/js/src/jsfun.h +++ b/js/src/jsfun.h @@ -324,6 +324,16 @@ class JSFunction : public js::NativeObject 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() { diff --git a/js/src/tests/ecma_6/Function/bound-length-and-name.js b/js/src/tests/ecma_6/Function/bound-length-and-name.js new file mode 100644 index 00000000000..ef2f1ffbcd6 --- /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'); diff --git a/js/src/tests/js1_8_5/extensions/scripted-proxies.js b/js/src/tests/js1_8_5/extensions/scripted-proxies.js index d385000c7d9..d3448ec8939 100644 --- a/js/src/tests/js1_8_5/extensions/scripted-proxies.js +++ b/js/src/tests/js1_8_5/extensions/scripted-proxies.js @@ -31,10 +31,15 @@ function test() { /* 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"); @@ -50,8 +55,15 @@ function test() { /* 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; })