summaryrefslogtreecommitdiffstats
path: root/js/src/vm/ArgumentsObject.h
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/ArgumentsObject.h')
-rw-r--r--js/src/vm/ArgumentsObject.h566
1 files changed, 566 insertions, 0 deletions
diff --git a/js/src/vm/ArgumentsObject.h b/js/src/vm/ArgumentsObject.h
new file mode 100644
index 0000000000..eeaca41a97
--- /dev/null
+++ b/js/src/vm/ArgumentsObject.h
@@ -0,0 +1,566 @@
+/* -*- 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/. */
+
+#ifndef vm_ArgumentsObject_h
+#define vm_ArgumentsObject_h
+
+#include "mozilla/MemoryReporting.h"
+
+#include "gc/Barrier.h"
+#include "gc/GCArray.h"
+#include "util/BitArray.h"
+#include "vm/NativeObject.h"
+
+namespace js {
+
+class AbstractFramePtr;
+class ArgumentsObject;
+class ScriptFrameIter;
+
+namespace jit {
+class JitFrameLayout;
+} // namespace jit
+
+// RareArgumentsData stores the deleted-elements bits for an arguments object.
+// Because |delete arguments[i]| is uncommon, we allocate this data the first
+// time an element is deleted.
+class RareArgumentsData {
+ // Pointer to an array of bits indicating, for every argument in
+ // [0, initialLength) whether the element has been deleted. See
+ // ArgumentsObject::isElementDeleted comment.
+ size_t deletedBits_[1];
+
+ RareArgumentsData() = default;
+ RareArgumentsData(const RareArgumentsData&) = delete;
+ void operator=(const RareArgumentsData&) = delete;
+
+ public:
+ static RareArgumentsData* create(JSContext* cx, ArgumentsObject* obj);
+ static size_t bytesRequired(size_t numActuals);
+
+ bool isElementDeleted(size_t len, size_t i) const {
+ MOZ_ASSERT(i < len);
+ return IsBitArrayElementSet(deletedBits_, len, i);
+ }
+ void markElementDeleted(size_t len, size_t i) {
+ MOZ_ASSERT(i < len);
+ SetBitArrayElement(deletedBits_, len, i);
+ }
+};
+
+// ArgumentsData stores the initial indexed arguments provided to a function
+// call. It is used to store arguments[i] -- up until the corresponding
+// property is modified, when the relevant value is flagged to memorialize the
+// modification.
+struct ArgumentsData {
+ RareArgumentsData* rareData = nullptr;
+
+ /*
+ * This array holds either the current argument value or the magic
+ * forwarding value. The latter means that the function has both a
+ * CallObject and an ArgumentsObject AND the particular formal variable is
+ * aliased by the CallObject. In such cases, the CallObject holds the
+ * canonical value so any element access to the arguments object should load
+ * the value out of the CallObject (which is pointed to by MAYBE_CALL_SLOT).
+ */
+ GCOwnedArray<Value> args;
+
+ /*
+ * numArgs = std::max(numFormalArgs, numActualArgs)
+ * The array 'args' has numArgs elements.
+ */
+ explicit ArgumentsData(uint32_t numArgs);
+
+ uint32_t numArgs() const { return args.size(); }
+
+ /* For jit use: */
+ static constexpr ptrdiff_t offsetOfArgs() {
+ return offsetof(ArgumentsData, args) +
+ GCOwnedArray<Value>::offsetOfElements();
+ }
+
+ static size_t bytesRequired(size_t numArgs) {
+ return offsetof(ArgumentsData, args) +
+ GCOwnedArray<Value>::bytesRequired(numArgs);
+ }
+};
+
+// Maximum supported value of arguments.length. This bounds the
+// maximum number of arguments that can be supplied to a spread call
+// or Function.prototype.apply. This value also bounds the number of
+// elements parsed in an array initializer. NB: keep this in sync
+// with the copy in builtin/SelfHostingDefines.h.
+static const unsigned ARGS_LENGTH_MAX = 500 * 1000;
+
+// Maximum number of arguments supported in jitcode. This bounds the
+// maximum number of arguments that can be supplied to a spread call
+// or Function.prototype.apply without entering the VM. We limit the
+// number of parameters we can handle to a number that does not risk
+// us allocating too much stack, notably on Windows where there is a
+// 4K guard page that has to be touched to extend the stack. The value
+// "3000" is the size of the guard page minus an arbitrary, but large,
+// safety margin. See bug 1351278.
+static const uint32_t JIT_ARGS_LENGTH_MAX = 3000 / sizeof(JS::Value);
+
+static_assert(JIT_ARGS_LENGTH_MAX <= ARGS_LENGTH_MAX,
+ "maximum jit arguments should be <= maximum arguments");
+
+/*
+ * [SMDOC] ArgumentsObject
+ *
+ * ArgumentsObject instances represent |arguments| objects created to store
+ * function arguments when a function is called. It's expensive to create such
+ * objects if they're never used, so they're only created when they are
+ * potentially used.
+ *
+ * Arguments objects are complicated because, for non-strict mode code, they
+ * must alias any named arguments which were provided to the function. Gnarly
+ * example:
+ *
+ * function f(a, b, c, d)
+ * {
+ * arguments[0] = "seta";
+ * assertEq(a, "seta");
+ * b = "setb";
+ * assertEq(arguments[1], "setb");
+ * c = "setc";
+ * assertEq(arguments[2], undefined);
+ * arguments[3] = "setd";
+ * assertEq(d, undefined);
+ * }
+ * f("arga", "argb");
+ *
+ * ES5's strict mode behaves more sanely, and named arguments don't alias
+ * elements of an arguments object.
+ *
+ * ArgumentsObject instances use the following reserved slots:
+ *
+ * INITIAL_LENGTH_SLOT
+ * Stores the initial value of arguments.length, plus a bit indicating
+ * whether arguments.length and/or arguments[@@iterator] have been
+ * modified. Use initialLength(), hasOverriddenLength(), and
+ * hasOverriddenIterator() to access these values. If arguments.length has
+ * been modified, then the current value of arguments.length is stored in
+ * another slot associated with a new property.
+ * DATA_SLOT
+ * Stores an ArgumentsData*, described above.
+ * MAYBE_CALL_SLOT
+ * Stores the CallObject, if the callee has aliased bindings. See
+ * the ArgumentsData::args comment.
+ * CALLEE_SLOT
+ * Stores the initial arguments.callee. This value can be overridden on
+ * mapped arguments objects, see hasOverriddenCallee.
+ */
+class ArgumentsObject : public NativeObject {
+ public:
+ static const uint32_t INITIAL_LENGTH_SLOT = 0;
+ static const uint32_t DATA_SLOT = 1;
+ static const uint32_t MAYBE_CALL_SLOT = 2;
+ static const uint32_t CALLEE_SLOT = 3;
+
+ static const uint32_t LENGTH_OVERRIDDEN_BIT = 0x1;
+ static const uint32_t ITERATOR_OVERRIDDEN_BIT = 0x2;
+ static const uint32_t ELEMENT_OVERRIDDEN_BIT = 0x4;
+ static const uint32_t CALLEE_OVERRIDDEN_BIT = 0x8;
+ static const uint32_t FORWARDED_ARGUMENTS_BIT = 0x10;
+ static const uint32_t PACKED_BITS_COUNT = 5;
+ static const uint32_t PACKED_BITS_MASK = (1 << PACKED_BITS_COUNT) - 1;
+
+ static_assert(ARGS_LENGTH_MAX <= (UINT32_MAX >> PACKED_BITS_COUNT),
+ "Max arguments length must fit in available bits");
+
+// Our ability to inline functions that use |arguments| is limited by
+// the number of registers available to represent Value operands to
+// CreateInlinedArgumentsObject.
+#if defined(JS_CODEGEN_X86)
+ static const uint32_t MaxInlinedArgs = 1;
+#else
+ static const uint32_t MaxInlinedArgs = 3;
+#endif
+
+ protected:
+ template <typename CopyArgs>
+ static ArgumentsObject* create(JSContext* cx, HandleFunction callee,
+ unsigned numActuals, CopyArgs& copy);
+
+ ArgumentsData* data() const {
+ return reinterpret_cast<ArgumentsData*>(
+ getFixedSlot(DATA_SLOT).toPrivate());
+ }
+
+ RareArgumentsData* maybeRareData() const { return data()->rareData; }
+
+ [[nodiscard]] bool createRareData(JSContext* cx);
+
+ RareArgumentsData* getOrCreateRareData(JSContext* cx) {
+ if (!data()->rareData && !createRareData(cx)) {
+ return nullptr;
+ }
+ return data()->rareData;
+ }
+
+ static bool obj_delProperty(JSContext* cx, HandleObject obj, HandleId id,
+ ObjectOpResult& result);
+
+ static bool obj_mayResolve(const JSAtomState& names, jsid id, JSObject*);
+
+ public:
+ static const uint32_t RESERVED_SLOTS = 4;
+ static const gc::AllocKind FINALIZE_KIND = gc::AllocKind::OBJECT4_BACKGROUND;
+
+ /* Create an arguments object for a frame that is expecting them. */
+ static ArgumentsObject* createExpected(JSContext* cx, AbstractFramePtr frame);
+
+ /*
+ * Purposefully disconnect the returned arguments object from the frame
+ * by always creating a new copy that does not alias formal parameters.
+ * This allows function-local analysis to determine that formals are
+ * not aliased and generally simplifies arguments objects.
+ */
+ static ArgumentsObject* createUnexpected(JSContext* cx,
+ ScriptFrameIter& iter);
+ static ArgumentsObject* createUnexpected(JSContext* cx,
+ AbstractFramePtr frame);
+
+ static ArgumentsObject* createForIon(JSContext* cx,
+ jit::JitFrameLayout* frame,
+ HandleObject scopeChain);
+ static ArgumentsObject* createForInlinedIon(JSContext* cx, Value* args,
+ HandleFunction callee,
+ HandleObject scopeChain,
+ uint32_t numActuals);
+ static ArgumentsObject* createFromValueArray(JSContext* cx,
+ HandleValueArray argsArray,
+ HandleFunction callee,
+ HandleObject scopeChain,
+ uint32_t numActuals);
+
+ private:
+ template <typename CopyArgs>
+ static ArgumentsObject* finishPure(JSContext* cx, ArgumentsObject* obj,
+ JSFunction* callee, JSObject* callObj,
+ unsigned numActuals, CopyArgs& copy);
+
+ public:
+ /*
+ * Allocate ArgumentsData and fill reserved slots after allocating an
+ * ArgumentsObject in Ion code.
+ */
+ static ArgumentsObject* finishForIonPure(JSContext* cx,
+ jit::JitFrameLayout* frame,
+ JSObject* scopeChain,
+ ArgumentsObject* obj);
+
+ /*
+ * Allocate ArgumentsData for inlined arguments and fill reserved slots after
+ * allocating an ArgumentsObject in Ion code.
+ */
+ static ArgumentsObject* finishInlineForIonPure(
+ JSContext* cx, JSObject* rawCallObj, JSFunction* rawCallee, Value* args,
+ uint32_t numActuals, ArgumentsObject* obj);
+
+ static ArgumentsObject* createTemplateObject(JSContext* cx, bool mapped);
+
+ /*
+ * Return the initial length of the arguments. This may differ from the
+ * current value of arguments.length!
+ */
+ uint32_t initialLength() const {
+ uint32_t argc = uint32_t(getFixedSlot(INITIAL_LENGTH_SLOT).toInt32()) >>
+ PACKED_BITS_COUNT;
+ MOZ_ASSERT(argc <= ARGS_LENGTH_MAX);
+ return argc;
+ }
+
+ // True iff arguments.length has been assigned or deleted.
+ bool hasOverriddenLength() const {
+ const Value& v = getFixedSlot(INITIAL_LENGTH_SLOT);
+ return v.toInt32() & LENGTH_OVERRIDDEN_BIT;
+ }
+
+ void markLengthOverridden() {
+ uint32_t v =
+ getFixedSlot(INITIAL_LENGTH_SLOT).toInt32() | LENGTH_OVERRIDDEN_BIT;
+ setFixedSlot(INITIAL_LENGTH_SLOT, Int32Value(v));
+ }
+
+ // Create the default "length" property and set LENGTH_OVERRIDDEN_BIT.
+ static bool reifyLength(JSContext* cx, Handle<ArgumentsObject*> obj);
+
+ // True iff arguments[@@iterator] has been assigned or deleted.
+ bool hasOverriddenIterator() const {
+ const Value& v = getFixedSlot(INITIAL_LENGTH_SLOT);
+ return v.toInt32() & ITERATOR_OVERRIDDEN_BIT;
+ }
+
+ void markIteratorOverridden() {
+ uint32_t v =
+ getFixedSlot(INITIAL_LENGTH_SLOT).toInt32() | ITERATOR_OVERRIDDEN_BIT;
+ setFixedSlot(INITIAL_LENGTH_SLOT, Int32Value(v));
+ }
+
+ // Create the default @@iterator property and set ITERATOR_OVERRIDDEN_BIT.
+ static bool reifyIterator(JSContext* cx, Handle<ArgumentsObject*> obj);
+
+ /*
+ * Return the arguments iterator function.
+ */
+ static bool getArgumentsIterator(JSContext* cx, MutableHandleValue val);
+
+ // True iff any element has been assigned or deleted.
+ bool hasOverriddenElement() const {
+ const Value& v = getFixedSlot(INITIAL_LENGTH_SLOT);
+ return v.toInt32() & ELEMENT_OVERRIDDEN_BIT;
+ }
+
+ void markElementOverridden() {
+ uint32_t v =
+ getFixedSlot(INITIAL_LENGTH_SLOT).toInt32() | ELEMENT_OVERRIDDEN_BIT;
+ setFixedSlot(INITIAL_LENGTH_SLOT, Int32Value(v));
+ }
+
+ private:
+ /*
+ * Because the arguments object is a real object, its elements may be
+ * deleted. This is implemented by setting a 'deleted' flag for the arg
+ * which is read by argument object resolve and getter/setter hooks.
+ *
+ * NB: an element, once deleted, stays deleted. Thus:
+ *
+ * function f(x) { delete arguments[0]; arguments[0] = 42; return x }
+ * assertEq(f(1), 1);
+ *
+ * This works because, once a property is deleted from an arguments object,
+ * it gets regular properties with regular getters/setters that don't alias
+ * ArgumentsData::args.
+ */
+ bool isElementDeleted(uint32_t i) const {
+ MOZ_ASSERT(i < data()->numArgs());
+ if (i >= initialLength()) {
+ return false;
+ }
+ bool result = maybeRareData() &&
+ maybeRareData()->isElementDeleted(initialLength(), i);
+ MOZ_ASSERT_IF(result, hasOverriddenElement());
+ return result;
+ }
+
+ protected:
+ bool markElementDeleted(JSContext* cx, uint32_t i);
+
+ public:
+ /*
+ * Return true iff the index is a valid element index for this arguments
+ * object.
+ *
+ * Returning true here doesn't imply that the element value can be read
+ * through |ArgumentsObject::element()|. For example unmapped arguments
+ * objects can have an element index property redefined without having marked
+ * the element as deleted. Instead use |maybeGetElement()| or manually check
+ * for |hasOverriddenElement()|.
+ */
+ bool isElement(uint32_t i) const {
+ return i < initialLength() && !isElementDeleted(i);
+ }
+
+ /*
+ * An ArgumentsObject serves two roles:
+ * - a real object, accessed through regular object operations, e.g..,
+ * GetElement corresponding to 'arguments[i]';
+ * - a VM-internal data structure, storing the value of arguments (formal
+ * and actual) that are accessed directly by the VM when a reading the
+ * value of a formal parameter.
+ * There are two ways to access the ArgumentsData::args corresponding to
+ * these two use cases:
+ * - object access should use elements(i) which will take care of
+ * forwarding when the value is the magic forwarding value;
+ * - VM argument access should use arg(i) which will assert that the
+ * value is not the magic forwarding value (since, if such forwarding was
+ * needed, the frontend should have emitted JSOp::GetAliasedVar).
+ */
+ const Value& element(uint32_t i) const;
+
+ inline void setElement(uint32_t i, const Value& v);
+
+ const Value& arg(unsigned i) const {
+ MOZ_ASSERT(i < data()->numArgs());
+ const Value& v = data()->args[i];
+ MOZ_ASSERT(!v.isMagic());
+ return v;
+ }
+
+ void setArg(unsigned i, const Value& v) {
+ MOZ_ASSERT(i < data()->numArgs());
+ MOZ_ASSERT(!data()->args[i].isMagic());
+ data()->args.setElement(this, i, v);
+ }
+
+ /*
+ * Test if an argument is forwarded, i.e. its actual value is stored in the
+ * CallObject and can't be directly read from |ArgumentsData::args|.
+ */
+ bool argIsForwarded(unsigned i) const {
+ MOZ_ASSERT(i < data()->numArgs());
+ const Value& v = data()->args[i];
+ MOZ_ASSERT_IF(IsMagicScopeSlotValue(v), anyArgIsForwarded());
+ return IsMagicScopeSlotValue(v);
+ }
+
+ bool anyArgIsForwarded() const {
+ const Value& v = getFixedSlot(INITIAL_LENGTH_SLOT);
+ return v.toInt32() & FORWARDED_ARGUMENTS_BIT;
+ }
+
+ void markArgumentForwarded() {
+ uint32_t v =
+ getFixedSlot(INITIAL_LENGTH_SLOT).toInt32() | FORWARDED_ARGUMENTS_BIT;
+ setFixedSlot(INITIAL_LENGTH_SLOT, Int32Value(v));
+ }
+
+ /*
+ * Attempt to speedily and efficiently access the i-th element of this
+ * arguments object. Return true if the element was speedily returned.
+ * Return false if the element must be looked up more slowly using
+ * getProperty or some similar method. The second overload copies the
+ * elements [start, start + count) into the locations starting at 'vp'.
+ *
+ * NB: Returning false does not indicate error!
+ */
+ bool maybeGetElement(uint32_t i, MutableHandleValue vp) {
+ if (i >= initialLength() || hasOverriddenElement()) {
+ return false;
+ }
+ vp.set(element(i));
+ return true;
+ }
+
+ inline bool maybeGetElements(uint32_t start, uint32_t count, js::Value* vp);
+
+ /*
+ * Measures things hanging off this ArgumentsObject that are counted by the
+ * |miscSize| argument in JSObject::sizeOfExcludingThis().
+ */
+ size_t sizeOfMisc(mozilla::MallocSizeOf mallocSizeOf) const {
+ if (!data()) { // Template arguments objects have no data.
+ return 0;
+ }
+ return mallocSizeOf(data()) + mallocSizeOf(maybeRareData());
+ }
+ size_t sizeOfData() const {
+ return ArgumentsData::bytesRequired(data()->numArgs()) +
+ (maybeRareData() ? RareArgumentsData::bytesRequired(initialLength())
+ : 0);
+ }
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj);
+ static void trace(JSTracer* trc, JSObject* obj);
+ static size_t objectMoved(JSObject* dst, JSObject* src);
+
+ /* For jit use: */
+ static size_t getDataSlotOffset() { return getFixedSlotOffset(DATA_SLOT); }
+ static size_t getInitialLengthSlotOffset() {
+ return getFixedSlotOffset(INITIAL_LENGTH_SLOT);
+ }
+
+ static Value MagicEnvSlotValue(uint32_t slot) {
+ // When forwarding slots to a backing CallObject, the slot numbers are
+ // stored as uint32 magic values. This raises an ambiguity if we have
+ // also copied JS_OPTIMIZED_OUT magic from a JIT frame or
+ // JS_UNINITIALIZED_LEXICAL magic on the CallObject. To distinguish
+ // normal magic values (those with a JSWhyMagic) and uint32 magic
+ // values, we add the maximum JSWhyMagic value to the slot
+ // number. This is safe as ARGS_LENGTH_MAX is well below UINT32_MAX.
+ static_assert(UINT32_MAX - JS_WHY_MAGIC_COUNT > ARGS_LENGTH_MAX);
+ return JS::MagicValueUint32(slot + JS_WHY_MAGIC_COUNT);
+ }
+ static uint32_t SlotFromMagicScopeSlotValue(const Value& v) {
+ static_assert(UINT32_MAX - JS_WHY_MAGIC_COUNT > ARGS_LENGTH_MAX);
+ return v.magicUint32() - JS_WHY_MAGIC_COUNT;
+ }
+ static bool IsMagicScopeSlotValue(const Value& v) {
+ return v.isMagic() && v.magicUint32() > JS_WHY_MAGIC_COUNT;
+ }
+
+ static void MaybeForwardToCallObject(AbstractFramePtr frame,
+ ArgumentsObject* obj,
+ ArgumentsData* data);
+ static void MaybeForwardToCallObject(JSFunction* callee, JSObject* callObj,
+ ArgumentsObject* obj,
+ ArgumentsData* data);
+};
+
+class MappedArgumentsObject : public ArgumentsObject {
+ static const JSClassOps classOps_;
+ static const ClassExtension classExt_;
+ static const ObjectOps objectOps_;
+
+ public:
+ static const JSClass class_;
+
+ JSFunction& callee() const {
+ return getFixedSlot(CALLEE_SLOT).toObject().as<JSFunction>();
+ }
+
+ bool hasOverriddenCallee() const {
+ const Value& v = getFixedSlot(INITIAL_LENGTH_SLOT);
+ return v.toInt32() & CALLEE_OVERRIDDEN_BIT;
+ }
+
+ void markCalleeOverridden() {
+ uint32_t v =
+ getFixedSlot(INITIAL_LENGTH_SLOT).toInt32() | CALLEE_OVERRIDDEN_BIT;
+ setFixedSlot(INITIAL_LENGTH_SLOT, Int32Value(v));
+ }
+
+ static size_t getCalleeSlotOffset() {
+ return getFixedSlotOffset(CALLEE_SLOT);
+ }
+
+ // Create the default "callee" property and set CALLEE_OVERRIDDEN_BIT.
+ static bool reifyCallee(JSContext* cx, Handle<MappedArgumentsObject*> obj);
+
+ private:
+ static bool obj_enumerate(JSContext* cx, HandleObject obj);
+ static bool obj_resolve(JSContext* cx, HandleObject obj, HandleId id,
+ bool* resolvedp);
+ static bool obj_defineProperty(JSContext* cx, HandleObject obj, HandleId id,
+ Handle<JS::PropertyDescriptor> desc,
+ ObjectOpResult& result);
+};
+
+class UnmappedArgumentsObject : public ArgumentsObject {
+ static const JSClassOps classOps_;
+ static const ClassExtension classExt_;
+
+ public:
+ static const JSClass class_;
+
+ private:
+ static bool obj_enumerate(JSContext* cx, HandleObject obj);
+ static bool obj_resolve(JSContext* cx, HandleObject obj, HandleId id,
+ bool* resolvedp);
+};
+
+extern bool MappedArgGetter(JSContext* cx, HandleObject obj, HandleId id,
+ MutableHandleValue vp);
+
+extern bool MappedArgSetter(JSContext* cx, HandleObject obj, HandleId id,
+ HandleValue v, ObjectOpResult& result);
+
+extern bool UnmappedArgGetter(JSContext* cx, HandleObject obj, HandleId id,
+ MutableHandleValue vp);
+
+extern bool UnmappedArgSetter(JSContext* cx, HandleObject obj, HandleId id,
+ HandleValue v, ObjectOpResult& result);
+
+} // namespace js
+
+template <>
+inline bool JSObject::is<js::ArgumentsObject>() const {
+ return is<js::MappedArgumentsObject>() || is<js::UnmappedArgumentsObject>();
+}
+
+#endif /* vm_ArgumentsObject_h */