summaryrefslogtreecommitdiffstats
path: root/js/src/vm/BoundFunctionObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/BoundFunctionObject.cpp')
-rw-r--r--js/src/vm/BoundFunctionObject.cpp536
1 files changed, 536 insertions, 0 deletions
diff --git a/js/src/vm/BoundFunctionObject.cpp b/js/src/vm/BoundFunctionObject.cpp
new file mode 100644
index 0000000000..1b80415598
--- /dev/null
+++ b/js/src/vm/BoundFunctionObject.cpp
@@ -0,0 +1,536 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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/. */
+
+#include "vm/BoundFunctionObject.h"
+
+#include <string_view>
+
+#include "util/StringBuffer.h"
+#include "vm/Interpreter.h"
+#include "vm/Shape.h"
+#include "vm/Stack.h"
+
+#include "gc/ObjectKind-inl.h"
+#include "vm/JSFunction-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+#include "vm/Shape-inl.h"
+
+using namespace js;
+
+// Helper function to initialize `args` with all bound arguments + the arguments
+// supplied in `callArgs`.
+template <typename Args>
+static MOZ_ALWAYS_INLINE void FillArguments(Args& args,
+ BoundFunctionObject* bound,
+ size_t numBoundArgs,
+ const CallArgs& callArgs) {
+ MOZ_ASSERT(args.length() == numBoundArgs + callArgs.length());
+
+ if (numBoundArgs <= BoundFunctionObject::MaxInlineBoundArgs) {
+ for (size_t i = 0; i < numBoundArgs; i++) {
+ args[i].set(bound->getInlineBoundArg(i));
+ }
+ } else {
+ ArrayObject* boundArgs = bound->getBoundArgsArray();
+ for (size_t i = 0; i < numBoundArgs; i++) {
+ args[i].set(boundArgs->getDenseElement(i));
+ }
+ }
+
+ for (size_t i = 0; i < callArgs.length(); i++) {
+ args[numBoundArgs + i].set(callArgs[i]);
+ }
+}
+
+// ES2023 10.4.1.1 [[Call]]
+// https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist
+// static
+bool BoundFunctionObject::call(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<BoundFunctionObject*> bound(cx,
+ &args.callee().as<BoundFunctionObject>());
+
+ // Step 1.
+ Rooted<Value> target(cx, bound->getTargetVal());
+
+ // Step 2.
+ Rooted<Value> boundThis(cx, bound->getBoundThis());
+
+ // Steps 3-4.
+ size_t numBoundArgs = bound->numBoundArgs();
+ InvokeArgs args2(cx);
+ if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) {
+ return false;
+ }
+ FillArguments(args2, bound, numBoundArgs, args);
+
+ // Step 5.
+ return Call(cx, target, boundThis, args2, args.rval());
+}
+
+// ES2023 10.4.1.2 [[Construct]]
+// https://tc39.es/ecma262/#sec-bound-function-exotic-objects-construct-argumentslist-newtarget
+// static
+bool BoundFunctionObject::construct(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ Rooted<BoundFunctionObject*> bound(cx,
+ &args.callee().as<BoundFunctionObject>());
+
+ MOZ_ASSERT(bound->isConstructor(),
+ "shouldn't have called this hook if not a constructor");
+
+ // Step 1.
+ Rooted<Value> target(cx, bound->getTargetVal());
+
+ // Step 2.
+ MOZ_ASSERT(IsConstructor(target));
+
+ // Steps 3-4.
+ size_t numBoundArgs = bound->numBoundArgs();
+ ConstructArgs args2(cx);
+ if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) {
+ return false;
+ }
+ FillArguments(args2, bound, numBoundArgs, args);
+
+ // Step 5.
+ Rooted<Value> newTarget(cx, args.newTarget());
+ if (newTarget == ObjectValue(*bound)) {
+ newTarget = target;
+ }
+
+ // Step 6.
+ Rooted<JSObject*> res(cx);
+ if (!Construct(cx, target, args2, newTarget, &res)) {
+ return false;
+ }
+ args.rval().setObject(*res);
+ return true;
+}
+
+// static
+JSString* BoundFunctionObject::funToString(JSContext* cx, Handle<JSObject*> obj,
+ bool isToSource) {
+ // Implementation of the funToString hook used by Function.prototype.toString.
+
+ // For the non-standard toSource extension, we include "bound" to indicate
+ // it's a bound function.
+ if (isToSource) {
+ static constexpr std::string_view nativeCodeBound =
+ "function bound() {\n [native code]\n}";
+ return NewStringCopy<CanGC>(cx, nativeCodeBound);
+ }
+
+ static constexpr std::string_view nativeCode =
+ "function() {\n [native code]\n}";
+ return NewStringCopy<CanGC>(cx, nativeCode);
+}
+
+// static
+SharedShape* BoundFunctionObject::assignInitialShape(
+ JSContext* cx, Handle<BoundFunctionObject*> obj) {
+ MOZ_ASSERT(obj->empty());
+
+ constexpr PropertyFlags propFlags = {PropertyFlag::Configurable};
+ if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().length,
+ LengthSlot, propFlags)) {
+ return nullptr;
+ }
+ if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().name,
+ NameSlot, propFlags)) {
+ return nullptr;
+ }
+
+ SharedShape* shape = obj->sharedShape();
+ if (shape->proto() == TaggedProto(&cx->global()->getFunctionPrototype())) {
+ cx->global()->setBoundFunctionShapeWithDefaultProto(shape);
+ }
+ return shape;
+}
+
+static MOZ_ALWAYS_INLINE bool ComputeLengthValue(
+ JSContext* cx, Handle<BoundFunctionObject*> bound, Handle<JSObject*> target,
+ size_t numBoundArgs, double* length) {
+ *length = 0.0;
+
+ // Try to avoid invoking the JSFunction resolve hook.
+ if (target->is<JSFunction>() &&
+ !target->as<JSFunction>().hasResolvedLength()) {
+ uint16_t targetLength;
+ if (!JSFunction::getUnresolvedLength(cx, target.as<JSFunction>(),
+ &targetLength)) {
+ return false;
+ }
+
+ if (size_t(targetLength) > numBoundArgs) {
+ *length = size_t(targetLength) - numBoundArgs;
+ }
+ return true;
+ }
+
+ // Use a fast path for getting the .length value if the target is a bound
+ // function with its initial shape.
+ Value targetLength;
+ if (target->is<BoundFunctionObject>() && target->shape() == bound->shape()) {
+ BoundFunctionObject* targetFn = &target->as<BoundFunctionObject>();
+ targetLength = targetFn->getLengthForInitialShape();
+ } else {
+ bool hasLength;
+ Rooted<PropertyKey> key(cx, NameToId(cx->names().length));
+ if (!HasOwnProperty(cx, target, key, &hasLength)) {
+ return false;
+ }
+
+ if (!hasLength) {
+ return true;
+ }
+
+ Rooted<Value> targetLengthRoot(cx);
+ if (!GetProperty(cx, target, target, key, &targetLengthRoot)) {
+ return false;
+ }
+ targetLength = targetLengthRoot;
+ }
+
+ if (targetLength.isNumber()) {
+ *length = std::max(
+ 0.0, JS::ToInteger(targetLength.toNumber()) - double(numBoundArgs));
+ }
+ return true;
+}
+
+static MOZ_ALWAYS_INLINE JSAtom* AppendBoundFunctionPrefix(JSContext* cx,
+ JSString* str) {
+ auto& cache = cx->zone()->boundPrefixCache();
+
+ JSAtom* strAtom = str->isAtom() ? &str->asAtom() : nullptr;
+ if (strAtom) {
+ if (auto p = cache.lookup(strAtom)) {
+ return p->value();
+ }
+ }
+
+ StringBuffer sb(cx);
+ if (!sb.append("bound ") || !sb.append(str)) {
+ return nullptr;
+ }
+ JSAtom* atom = sb.finishAtom();
+ if (!atom) {
+ return nullptr;
+ }
+
+ if (strAtom) {
+ (void)cache.putNew(strAtom, atom);
+ }
+ return atom;
+}
+
+static MOZ_ALWAYS_INLINE JSAtom* ComputeNameValue(
+ JSContext* cx, Handle<BoundFunctionObject*> bound,
+ Handle<JSObject*> target) {
+ // Try to avoid invoking the JSFunction resolve hook.
+ JSString* name = nullptr;
+ if (target->is<JSFunction>() && !target->as<JSFunction>().hasResolvedName()) {
+ JSFunction* targetFn = &target->as<JSFunction>();
+ name = targetFn->getUnresolvedName(cx);
+ if (!name) {
+ return nullptr;
+ }
+ } else {
+ // Use a fast path for getting the .name value if the target is a bound
+ // function with its initial shape.
+ Value targetName;
+ if (target->is<BoundFunctionObject>() &&
+ target->shape() == bound->shape()) {
+ BoundFunctionObject* targetFn = &target->as<BoundFunctionObject>();
+ targetName = targetFn->getNameForInitialShape();
+ } else {
+ Rooted<Value> targetNameRoot(cx);
+ if (!GetProperty(cx, target, target, cx->names().name, &targetNameRoot)) {
+ return nullptr;
+ }
+ targetName = targetNameRoot;
+ }
+ if (!targetName.isString()) {
+ return cx->names().boundWithSpace_;
+ }
+ name = targetName.toString();
+ }
+
+ return AppendBoundFunctionPrefix(cx, name);
+}
+
+// ES2023 20.2.3.2 Function.prototype.bind
+// https://tc39.es/ecma262/#sec-function.prototype.bind
+// static
+bool BoundFunctionObject::functionBind(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ // Steps 1-2.
+ if (!IsCallable(args.thisv())) {
+ ReportIncompatibleMethod(cx, args, &FunctionClass);
+ return false;
+ }
+
+ if (MOZ_UNLIKELY(args.length() > ARGS_LENGTH_MAX)) {
+ JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+ JSMSG_TOO_MANY_ARGUMENTS);
+ return false;
+ }
+
+ Rooted<JSObject*> target(cx, &args.thisv().toObject());
+
+ BoundFunctionObject* bound =
+ functionBindImpl(cx, target, args.array(), args.length(), nullptr);
+ if (!bound) {
+ return false;
+ }
+
+ // Step 11.
+ args.rval().setObject(*bound);
+ return true;
+}
+
+// ES2023 20.2.3.2 Function.prototype.bind
+// https://tc39.es/ecma262/#sec-function.prototype.bind
+//
+// ES2023 10.4.1.3 BoundFunctionCreate
+// https://tc39.es/ecma262/#sec-boundfunctioncreate
+//
+// BoundFunctionCreate has been inlined in Function.prototype.bind for
+// performance reasons.
+//
+// static
+BoundFunctionObject* BoundFunctionObject::functionBindImpl(
+ JSContext* cx, Handle<JSObject*> target, Value* args, uint32_t argc,
+ Handle<BoundFunctionObject*> maybeBound) {
+ MOZ_ASSERT(target->isCallable());
+
+ // Make sure the arguments on the stack are rooted when we're called directly
+ // from JIT code.
+ RootedExternalValueArray argsRoot(cx, argc, args);
+
+ size_t numBoundArgs = argc > 0 ? argc - 1 : 0;
+ MOZ_ASSERT(numBoundArgs <= ARGS_LENGTH_MAX, "ensured by callers");
+
+ // If this assertion fails, make sure we use the correct AllocKind and that we
+ // use all of its slots (consider increasing MaxInlineBoundArgs).
+ static_assert(gc::GetGCKindSlots(allocKind) == SlotCount);
+
+ // ES2023 10.4.1.3 BoundFunctionCreate
+ // Steps 1-5.
+ Rooted<BoundFunctionObject*> bound(cx);
+ if (maybeBound) {
+ // We allocated a bound function in JIT code. In the uncommon case of the
+ // target not having Function.prototype as proto, we have to set the right
+ // proto here.
+ bound = maybeBound;
+ if (MOZ_UNLIKELY(bound->staticPrototype() != target->staticPrototype())) {
+ Rooted<JSObject*> proto(cx, target->staticPrototype());
+ if (!SetPrototype(cx, bound, proto)) {
+ return nullptr;
+ }
+ }
+ } else {
+ // Step 1.
+ Rooted<JSObject*> proto(cx);
+ if (!GetPrototype(cx, target, &proto)) {
+ return nullptr;
+ }
+
+ // Steps 2-5.
+ if (proto == &cx->global()->getFunctionPrototype() &&
+ cx->global()->maybeBoundFunctionShapeWithDefaultProto()) {
+ Rooted<SharedShape*> shape(
+ cx, cx->global()->maybeBoundFunctionShapeWithDefaultProto());
+ bound = NativeObject::create<BoundFunctionObject>(
+ cx, allocKind, gc::Heap::Default, shape);
+ if (!bound) {
+ return nullptr;
+ }
+ } else {
+ bound = NewObjectWithGivenProto<BoundFunctionObject>(cx, proto);
+ if (!bound) {
+ return nullptr;
+ }
+ if (!SharedShape::ensureInitialCustomShape<BoundFunctionObject>(cx,
+ bound)) {
+ return nullptr;
+ }
+ }
+ }
+
+ MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot);
+ MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot);
+
+ // Steps 6 and 9.
+ bound->initFlags(numBoundArgs, target->isConstructor());
+
+ // Step 7.
+ bound->initReservedSlot(TargetSlot, ObjectValue(*target));
+
+ // Step 8.
+ if (argc > 0) {
+ bound->initReservedSlot(BoundThisSlot, args[0]);
+ }
+
+ if (numBoundArgs <= MaxInlineBoundArgs) {
+ for (size_t i = 0; i < numBoundArgs; i++) {
+ bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]);
+ }
+ } else {
+ ArrayObject* arr = NewDenseCopiedArray(cx, numBoundArgs, args + 1);
+ if (!arr) {
+ return nullptr;
+ }
+ bound->initReservedSlot(BoundArg0Slot, ObjectValue(*arr));
+ }
+
+ // ES2023 20.2.3.2 Function.prototype.bind
+ // Step 4.
+ double length = 0.0;
+
+ // Steps 5-6.
+ if (!ComputeLengthValue(cx, bound, target, numBoundArgs, &length)) {
+ return nullptr;
+ }
+
+ // Step 7.
+ bound->initLength(length);
+
+ // Steps 8-9.
+ JSAtom* name = ComputeNameValue(cx, bound, target);
+ if (!name) {
+ return nullptr;
+ }
+
+ // Step 10.
+ bound->initName(name);
+
+ // Step 11.
+ return bound;
+}
+
+// static
+BoundFunctionObject* BoundFunctionObject::createWithTemplate(
+ JSContext* cx, Handle<BoundFunctionObject*> templateObj) {
+ Rooted<SharedShape*> shape(cx, templateObj->sharedShape());
+ auto* bound = NativeObject::create<BoundFunctionObject>(
+ cx, allocKind, gc::Heap::Default, shape);
+ if (!bound) {
+ return nullptr;
+ }
+ bound->initFlags(templateObj->numBoundArgs(), templateObj->isConstructor());
+ bound->initLength(templateObj->getLengthForInitialShape().toInt32());
+ bound->initName(&templateObj->getNameForInitialShape().toString()->asAtom());
+ return bound;
+}
+
+// static
+BoundFunctionObject* BoundFunctionObject::functionBindSpecializedBaseline(
+ JSContext* cx, Handle<JSObject*> target, Value* args, uint32_t argc,
+ Handle<BoundFunctionObject*> templateObj) {
+ // Root the Values on the stack.
+ RootedExternalValueArray argsRoot(cx, argc, args);
+
+ MOZ_ASSERT(target->is<JSFunction>() || target->is<BoundFunctionObject>());
+ MOZ_ASSERT(target->isCallable());
+ MOZ_ASSERT(target->isConstructor() == templateObj->isConstructor());
+ MOZ_ASSERT(target->staticPrototype() == templateObj->staticPrototype());
+
+ size_t numBoundArgs = argc > 0 ? argc - 1 : 0;
+ MOZ_ASSERT(numBoundArgs <= MaxInlineBoundArgs);
+
+ BoundFunctionObject* bound = createWithTemplate(cx, templateObj);
+ if (!bound) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot);
+ MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot);
+
+ bound->initReservedSlot(TargetSlot, ObjectValue(*target));
+ if (argc > 0) {
+ bound->initReservedSlot(BoundThisSlot, args[0]);
+ }
+ for (size_t i = 0; i < numBoundArgs; i++) {
+ bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]);
+ }
+ return bound;
+}
+
+// static
+BoundFunctionObject* BoundFunctionObject::createTemplateObject(JSContext* cx) {
+ Rooted<JSObject*> proto(cx, &cx->global()->getFunctionPrototype());
+ Rooted<BoundFunctionObject*> bound(
+ cx, NewTenuredObjectWithGivenProto<BoundFunctionObject>(cx, proto));
+ if (!bound) {
+ return nullptr;
+ }
+ if (!SharedShape::ensureInitialCustomShape<BoundFunctionObject>(cx, bound)) {
+ return nullptr;
+ }
+ return bound;
+}
+
+bool BoundFunctionObject::initTemplateSlotsForSpecializedBind(
+ JSContext* cx, uint32_t numBoundArgs, bool targetIsConstructor,
+ uint32_t targetLength, JSAtom* targetName) {
+ size_t len = 0;
+ if (targetLength > numBoundArgs) {
+ len = targetLength - numBoundArgs;
+ }
+
+ JSAtom* name = AppendBoundFunctionPrefix(cx, targetName);
+ if (!name) {
+ return false;
+ }
+
+ initFlags(numBoundArgs, targetIsConstructor);
+ initLength(len);
+ initName(name);
+ return true;
+}
+
+static const JSClassOps classOps = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ nullptr, // finalize
+ BoundFunctionObject::call, // call
+ BoundFunctionObject::construct, // construct
+ nullptr, // trace
+};
+
+static const ObjectOps objOps = {
+ nullptr, // lookupProperty
+ nullptr, // qdefineProperty
+ nullptr, // hasProperty
+ nullptr, // getProperty
+ nullptr, // setProperty
+ nullptr, // getOwnPropertyDescriptor
+ nullptr, // deleteProperty
+ nullptr, // getElements
+ BoundFunctionObject::funToString, // funToString
+};
+
+const JSClass BoundFunctionObject::class_ = {
+ "BoundFunctionObject",
+ // Note: bound functions don't have their own constructor or prototype (they
+ // use the prototype of the target object), but we give them a JSProtoKey
+ // because that's what Xray wrappers use to identify builtin objects.
+ JSCLASS_HAS_CACHED_PROTO(JSProto_BoundFunction) |
+ JSCLASS_HAS_RESERVED_SLOTS(BoundFunctionObject::SlotCount),
+ &classOps,
+ JS_NULL_CLASS_SPEC,
+ JS_NULL_CLASS_EXT,
+ &objOps,
+};