diff options
Diffstat (limited to 'js/src/vm/ArgumentsObject.h')
-rw-r--r-- | js/src/vm/ArgumentsObject.h | 566 |
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 */ |